1 线程和进程的区别
-
资源分配和调度:
- 进程(火车)是操作系统进行资源分配和调度的最小单位。它有自己的独立资源空间,包括内存、文件句柄等。
- 线程(车厢)是CPU调度的最小单位。一个进程可以包含多个线程,它们共享相同的资源空间,如内存,但有各自的执行路径。
-
数据共享:
- 进程间的数据共享相对困难,需要使用进程间通信(IPC)的机制。
- 同一进程内的不同线程可以直接共享数据,因为它们共享相同的内存空间。
-
稳定性和影响范围:
- 进程间是相互独立的,一个进程的崩溃通常不会影响其他进程。
- 同一进程下的线程是共享相同的进程资源,一个线程的崩溃可能导致整个进程崩溃。
-
资源消耗和扩展性:
- 进程比线程消耗更多的计算机资源,因为它们有独立的资源空间。
- 进程可以在多台机器上运行,而线程适合在多核处理器上运行。
-
内存地址管理:
- 进程的内存地址可以上锁,实现互斥访问,确保线程安全。
- 进程的内存地址可以限定使用量,通过信号量等机制控制资源的访问。
这个比喻涵盖了进程和线程的许多关键概念,希望能够更好地帮助理解它们之间的区别和关系。
2 基础线程创建
2.1 代码示例
以下最简单的事例来理解线程:
cpp
//test.cpp
#include <iostream>
#include <pthread.h> //必须的头文件
using namespace std;
#define NUM_THREADS 5
// 线程的运行函数
void* say_hello(void* args)
{
cout << "Hello Runoob!" << endl;
return 0;
}
int main()
{
// 定义线程的 id 变量,多个变量使用数组
pthread_t tids[NUM_THREADS];
for(int i = 0; i < NUM_THREADS; ++i)
{
//参数依次是:创建的线程id,线程参数,调用的函数,传入的函数参数
int ret = pthread_create(&tids[i], NULL, say_hello, NULL);
if (ret != 0)
{
cout << "pthread_create error: error_code=" << ret << endl;
}
}
//等各个线程退出后,进程才结束,否则进程强制结束了,线程可能还没反应过来;
//**当linux中,主线程以pthread_exit(NULL)作为返回值,则主线程会等待子线程。**
pthread_exit(NULL);
}
2.2 线程代码编译
2.2.1 命令行编译
使用 -lpthread 库编译下面的程序:
$ g++ test.cpp -lpthread -o test.o
2.2.2 Makefile编译
但是如果用Makefile一堆文件编译的情况下如何设置
# Makefile中变量定义如:标签=内容,后面的变量引用需要$(标签)
GPU=0
CUDNN=0
OPENCV=0
OPENMP=0 # OpenMP是一套支持跨平台共享内存方式的多线程并发的编程API
DEBUG=0
ARCH= -gencode arch=compute_30,code=sm_30 \
-gencode arch=compute_35,code=sm_35 \
-gencode arch=compute_50,code=[sm_50,compute_50] \
-gencode arch=compute_52,code=[sm_52,compute_52]
# -gencode arch=compute_20,code=[sm_20,sm_21] \ This one is deprecated?
# This is what I use, uncomment if you know your arch and want to specify
# ARCH= -gencode arch=compute_52,code=compute_52
VPATH=./src/:./examples
SLIB=libdarknet.so #待生成的动态链接库
ALIB=libdarknet.a #待生成的静态链接库
EXEC=darknet #待生成的执行文件名称
OBJDIR=./obj/ #生成目标文件的目录,即当期目录下obj子目录
CC=gcc
CPP=g++
NVCC=nvcc #nvidia编译器
AR=ar #ar,Linux系统的一个备份压缩命令,用于创建、修改备存文件(archive)。这里的ar命令是将目标文件打包为静态链接库。
ARFLAGS=rcs #ar命令的参数 -rcs。-r:将文件插入备存文件中 c:建立备存文件 s:若备存文件中包含了对象模式,可利用此参数建立备存文件的符号表
OPTS=-Ofast
#编译选项中指定 -pthread 会附加一个宏定义 -D_REENTRANT,该宏会导致 libc 头文件选择那些thread-safe的实现;
#链接选项中指定 -pthread 则同 -lpthread 一样,只表示链接 POSIX thread 库。由于 libc 用于适应 thread-safe 的宏定义可能变化,
#因此在编译和链接时都使用 -pthread 选项而不是传统的 -lpthread 能够保持向后兼容,并提高命令行的一致性。
LDFLAGS= -lm -pthread
COMMON= -Iinclude/ -Isrc/
#-fPIC:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。
#unknow-pragmas:使用未知的#pragma指令
CFLAGS=-Wall -Wno-unused-result -Wno-unknown-pragmas -Wfatal-errors -fPIC
ifeq ($(OPENMP), 1) #条件判断
CFLAGS+= -fopenmp
endif
ifeq ($(DEBUG), 1)
OPTS=-O0 -g #-g选项可以产生带有调试信息的目标代码
endif
CFLAGS+=$(OPTS)
ifeq ($(OPENCV), 1)
COMMON+= -DOPENCV #相当于C语言中的#define OPENCV
CFLAGS+= -DOPENCV
LDFLAGS+= `pkg-config --libs opencv` -lstdc++ #pkg-config能够把头文件和库文件的位置指出来,给编译器使用
COMMON+= `pkg-config --cflags opencv`
endif
ifeq ($(GPU), 1)
COMMON+= -DGPU -I/usr/local/cuda/include/
CFLAGS+= -DGPU
LDFLAGS+= -L/usr/local/cuda/lib64 -lcuda -lcudart -lcublas -lcurand
endif
ifeq ($(CUDNN), 1)
COMMON+= -DCUDNN
CFLAGS+= -DCUDNN
LDFLAGS+= -lcudnn
endif
#OBJ表示需要生成的目标文件
OBJ=gemm.o utils.o cuda.o deconvolutional_layer.o convolutional_layer.o list.o image.o activations.o im2col.o col2im.o blas.o crop_layer.o dropout_layer.o maxpool_layer.o softmax_layer.o data.o matrix.o network.o connected_layer.o cost_layer.o parser.o option_list.o detection_layer.o route_layer.o upsample_layer.o box.o normalization_layer.o avgpool_layer.o layer.o local_layer.o shortcut_layer.o logistic_layer.o activation_layer.o rnn_layer.o gru_layer.o crnn_layer.o demo.o batchnorm_layer.o region_layer.o reorg_layer.o tree.o lstm_layer.o l2norm_layer.o yolo_layer.o iseg_layer.o image_opencv.o
EXECOBJA=captcha.o lsd.o super.o art.o tag.o cifar.o go.o rnn.o segmenter.o regressor.o classifier.o coco.o yolo.o detector.o nightmare.o instance-segmenter.o darknet.o
ifeq ($(GPU), 1)
LDFLAGS+= -lstdc++
OBJ+=convolutional_kernels.o deconvolutional_kernels.o activation_kernels.o im2col_kernels.o col2im_kernels.o blas_kernels.o crop_layer_kernels.o dropout_layer_kernels.o maxpool_layer_kernels.o avgpool_layer_kernels.o
endif
EXECOBJ = $(addprefix $(OBJDIR), $(EXECOBJA)) #addprefix统一添加前缀
OBJS = $(addprefix $(OBJDIR), $(OBJ))
DEPS = $(wildcard src/*.h) Makefile include/darknet.h #wildcard展开src文件下所有后缀为.h的头文件;
all: obj backup results $(SLIB) $(ALIB) $(EXEC) #主要任务all和其所有依赖
#all: obj results $(SLIB) $(ALIB) $(EXEC)
$(EXEC): $(EXECOBJ) $(ALIB)
$(CC) $(COMMON) $(CFLAGS) $^ -o $@ $(LDFLAGS) $(ALIB) #$@自动变量,表示目标名称,这里指$(EXEC),实际为darknet;$^指代所有前置条件,之间以空格分隔
$(ALIB): $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(SLIB): $(OBJS)
$(CC) $(CFLAGS) -shared $^ -o $@ $(LDFLAGS) #-shared 该选项指定生成动态连接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号),不用该标志外部程序无法连接。相当于一个可执行文件
$(OBJDIR)%.o: %.cpp $(DEPS)
$(CPP) $(COMMON) $(CFLAGS) -c $< -o $@ #$<指代第一个前置条件,这里指%.cpp; -c编译、汇编到目标代码,不进行链接
$(OBJDIR)%.o: %.c $(DEPS)
$(CC) $(COMMON) $(CFLAGS) -c $< -o $@
$(OBJDIR)%.o: %.cu $(DEPS)
$(NVCC) $(ARCH) $(COMMON) --compiler-options "$(CFLAGS)" -c $< -o $@
obj:
mkdir -p obj #-p 确保目录名称存在,不存在的就建一个
backup:
mkdir -p backup
results:
mkdir -p results
.PHONY: clean
clean:
rm -rf $(OBJS) $(SLIB) $(ALIB) $(EXEC) $(EXECOBJ) $(OBJDIR)/*
3 线程属性
cpp
1、pthread_attr_init
功能: 对线程属性变量的初始化。
2、pthread_attr_setscope
功能: 设置线程 __scope 属性。scope属性表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同进程中的线程竞争CPU。默认为PTHREAD_SCOPE_PROCESS。目前LinuxThreads仅实现了PTHREAD_SCOPE_SYSTEM一值。
3、pthread_attr_setdetachstate
功能: 设置线程detachstate属性。该表示新线程是否与进程中其他线程脱离同步,如果设置为PTHREAD_CREATE_DETACHED则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为PTHREAD_CREATE_JOINABLE状态。这个属性也可以在线程创建并运行以后用pthread_detach()来设置,而一旦设置为PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE状态。
4、pthread_attr_setschedparam
功能: 设置线程schedparam属性,即调用的优先级。
5、pthread_attr_getschedparam
功能: 得到线程优先级。
cpp
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <pthread.h>
static void pthread_func_1 (void);
static void pthread_func_2 (void);
int main (int argc, char** argv)
{
pthread_t pt_1 = 0;
pthread_t pt_2 = 0;
pthread_attr_t atrr = {0};
int ret = 0;
//初始化属性线程属性
pthread_attr_init (&attr);
pthread_attr_setscope (&attr, PTHREAD_SCOPE_SYSTEM);
pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);
ret = pthread_create (&pt_1, &attr, pthread_func_1, NULL);
if (ret != 0)
{
perror ("pthread_1_create");
}
**//线程一的线程函数一结束就自动释放资源(因为设置了分离属性)**
ret = pthread_create (&pt_2, NULL, pthread_func_2, NULL);
if (ret != 0)
{
perror ("pthread_2_create");
}
**//线程二就得等到pthread_join来释放系统资源(因为没有设置分离属性,需要依赖主线程释放资源)**
pthread_join (pt_2, NULL);
return 0;
}
static void pthread_func_1 (void)
{
int i = 0;
for (; i < 6; i++)
{
printf ("This is pthread_1.\n");
if (i == 2)
{
pthread_exit (0);
}
}
return;
}
static void pthread_func_2 (void)
{
int i = 0;
for (; i < 3; i ++)
{
printf ("This is pthread_2.\n");
}
return;
}
4 注意内存泄漏问题
在使用pthread_create
函数创建线程时,默认情况下线程是非分离属性的,其分离属性由pthread_create
函数的第二个参数决定。在非分离的情况下,当一个线程结束时,它所占用的系统资源并没有完全释放,也没有真正终止。
- 只有在调用
pthread_join
函数并在其返回时,该线程才会释放自己的资源。 - 或者在设置为分离属性的情况下,一个线程结束会立即释放它所占用的资源。
为了确保在创建线程后避免内存泄漏,必须规范地使用pthread_create
,具体做法是设置线程为分离属性。以下是一个示例代码:
cpp
void run() {
return;
}
int main(){
pthread_t thread; // 创建线程id
pthread_attr_t attr; // 设置线程属性
pthread_attr_init(&attr); // 初始化线程属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置分离属性
// 也可以使用 pthread_attr_setdetachstate(&attr, 1); 来设置分离属性
pthread_create(&thread, &attr, run, 0); // 第二个参数决定了分离属性
// ......
return 0;
}
通过设置线程为分离属性,可以确保在线程结束时即刻释放其占用的资源,有效地防止内存泄漏问题。
5 总结
本文深入探讨了线程和进程的概念,以及它们之间的关键区别。通过一个简单而生动的比喻,将进程比喻为火车,线程比喻为火车的车厢,为读者提供了直观的理解。总体而言,深入理解线程和进程的概念,并掌握基础线程创建和属性设置,对于并发编程和多线程应用的开发至关重要。通过本文的阐述,读者将更好地理解这些概念,并能够在实际应用中更有效地利用线程和进程的优势。