本文主要是进程——线程的创建与回收的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows windows11 Ubuntu Ubuntu16.04的64位版本 VMware® Workstation 16 Pro 16.2.3 build-19376536 SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 开发板 正点原子 i.MX6ULL Linux阿尔法开发板 uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) linux内核 linux-4.15(NXP官方提供) STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
点击查看相关文件下载
一、线程的创建 1. 如何创建线程? 1.1 pthread_create() 1.1.1 函数说明 在 linux 下可以使用 man pthread_create 命令查看该函数的帮助手册。
1 2 3 #include <pthread.h> int pthread_create (pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg) ;
【函数说明】 该函数创建一个新的线程,创建出来的新线程被称为主线程的子线程。
【函数参数】
thread : pthread_t * 类型,当 pthread_create() 成功返回时,新创建的线程的线程 ID 会保存在参数 thread 所指向的内存中,后续的线程相关函数会使用该标识来引用此线程。
attr : pthread_attr_t * 类型,指向 pthread_attr_t 类型的缓冲区, pthread_attr_t 数据类型定义了线程的各种属性。如果将参数 attr 设置为 NULL ,那么表示将线程的所有属性设置为默认值,以此创建新线程。
start_routine :参数 start_routine 是一个函数指针,指向一个函数,新创建的线程从 start_routine() 函数开始运行,该函数返回值类型为 void * ,并且该函数的参数只有一个 void * ,其实这个参数就是 pthread_create() 函数的第四个参数 arg 。如果需要向 start_routine() 传递的参数有一个以上,那么需要把这些参数放到一个结构体中,然后把这个结构体对象的地址作为 arg 参数传入。
点击查看该形参的详细解释
这个参数是什么,到底指向什么?
1 void *(*start_routine) (void *)
(1)*start_routine 表示这是一个指针变量,指针变量名为 start_routine ;
(2)后边的 (void *) 表示这个指针变量可以指向带有一个 void * 类型形参的函数;
(3)前边的 void * 表示这个指针变量可以指向返回值为 void * 类型的函数;
总的说起来就是,这句话定义了一个可以指向带一个 void * 参数且返回值为 void * 类型的函数的指针变量 start_routine 。
arg :传递给 start_routine() 函数的参数。一般情况下,需要将 arg 指向一个全局或堆变量,意思就是说在线程的生命周期中,该 arg 指向的对象必须存在,否则如果线程中访问了该对象将会出现错误。当然也可将参数 arg 设置为 NULL ,表示不需要传入参数给 start_routine() 函数。
【返回值】 int 类型,成功返回 0 ;失败时将返回一个错误号,并且参数 thread 指向的内容是不确定的。
【使用格式】 一般情况下基本使用格式如下:
1 2 3 4 5 6 7 #include <pthread.h> int ret;pthread_t tid; ret = pthread_create(&tid, NULL , (void *)testThread, NULL );
【注意事项】
(1) pthread_create() 在调用失败时通常会返回错误码,它并不像其它库函数或系统调用一样设置 errno ,每个线程都提供了全局变量 errno 的副本,这只是为了与使用 errno 到的函数进行兼容,在线程中,从函数中返回错误码更为清晰整洁,不需要依赖那些随着函数执行不断变化的全局变量,这样可以把错误的范围限制在引起出错的函数中。
(2)线程创建成功,新线程就会加入到系统调度队列中,获取到 CPU 之后就会立马从 start_routine() 函数开始运行该线程的任务;调用 pthread_create() 函数后,通常我们无法确定系统接着会调度哪一个线程来使用 CPU 资源,先调度主线程还是新创建的线程呢(而在多核 CPU 或多 CPU 系统中,多核线程可能会在不同的核心上同时执行)?如果程序对执行顺序有强制要求,那么就必须采用一些同步技术来实现 。
1.1.2 使用实例 点击查看实例
【说明】主线程休眠了 1 秒钟,原因在于,如果主线程不进行休眠,它就可能会立马退出,这样可能会导致新创建的线程还没有机会运行,整个进程就结束了。
test.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <stdio.h> #include <pthread.h> #include <unistd.h> int *testThread (void *arg) ;int main (int argc, char *argv[]) { int ret; pthread_t tid; ret = pthread_create(&tid, NULL , (void *)testThread, NULL ); printf ("This is main thread,ret=%d,tid=%lu,pthread_self=%lu,pid=%d\n" , ret, tid, pthread_self(), getpid()); sleep(3 ); return 0 ; } int *testThread (void *arg) { printf ("This is child thread,pthread_self=%lu,pid=%d\n" ,pthread_self(), getpid()); return NULL ; }
在终端执行以下命令编译程序:
1 gcc test.c -Wall # 生成可执行文件 a.out
要是没啥意外的话,应该会报以下 error :
1 2 3 /usr/bin/ld: /tmp/ccE9ZZGr.o: in function main': test.c:(.text+0x3b): undefined reference to pthread_create' collect2: error: ld returned 1 exit status
我们明明包含了相关头文件啊?前边 man 查看手册的时候,有这么一句话 Compile and link with -pthread. ,这便是问题所在,我们编译程序的时候需要加上 prhread 链接库就可以啦( -l 与 pthread 之间有无空格都可以):
1 2 gcc test.c -Wall -l pthread # 生成可执行文件 a.out ./a.out # 执行可执行程序
然后,终端会有以下信息显示:
1 2 This is main thread,ret=0,tid=139924118435392,pthread_self=139924121261888,pid=4119 This is child thread,pthread_self=139924118435392,pid=4119
2. 参数的传递 2.1 地址传递 2.1.1 应该怎么写? 1 2 3 4 5 6 7 8 int *testThread (void *arg) ;pthread_create(&tid, NULL , (void *)testThread, NULL ); int pthread_create (pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg) ;
在上边的 pthread_create 函数中,我们知道最后的 arg 参数是可以传入 start_routine 指向的函数的,他们参数的类型都是 void * ,既然是指针,我们可以定义一个变量,传入地址即可。若是定义的变量为整型,那么定义变量→传入参数→使用参数的格式解读如下:
1 2 3 4 5 6 7 8 int arg = 1 ;(void *)&arg *(int *)arg
2.1.2 使用实例1 点击查看实例1
test.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #include <pthread.h> #include <unistd.h> void *testThreadParam1 (void *arg) ;int main (int argc, char *argv[]) { int ret; pthread_t tid; int arg = 5 ; ret = pthread_create(&tid, NULL , testThreadParam1, (void *)&arg); printf ("This is main thread,ret=%d,tid=%lu\n" , ret, tid); sleep(1 ); return 0 ; } void *testThreadParam1 (void *arg) { printf ("This is a new thread test! pid=%d,tid=%lu\n" , getpid(), pthread_self()); printf ("input arg=%d\n" , *(int *)arg); return NULL ; }
在终端执行以下命令编译程序:
1 2 gcc test.c -Wall -l pthread # 生成可执行文件 a.out ./a.out # 执行可执行程序
然后,终端会有以下信息显示:
1 2 3 This is main thread,ret=0,tid=139903662814784 This is a new thread test! pid=4472,tid=139903662814784 input arg=5
通过上边的实例,我们会发现,变量做了正确的强制类型转换,正常的按地址传递,没有警告,参数的传递也没有问题。
2.1.2 使用实例2 我们再来试一下创建 5 个子线程,并将参数传递进去,我们来看一看会发生什么现象。
点击查看实例2
test.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <stdio.h> #include <pthread.h> #include <unistd.h> void *testThreadParam (void *arg) ;int main (int argc, char *argv[]) { int ret; pthread_t tid[5 ]; int i = 0 ; for (i = 0 ; i < 5 ; i++) { ret = pthread_create(&tid[i], NULL , testThreadParam, (void *)&i); printf ("This is %d thread create,ret=%d,tid=%lu\n" , i, ret, tid[i]); } sleep(5 ); return 0 ; } void *testThreadParam (void *arg) { printf ("This is %d new thread.tid=%lu\n" , *(int *)arg, pthread_self()); return NULL ; }
在终端执行以下命令编译程序:
1 2 gcc test.c -Wall -l pthread # 生成可执行文件 a.out ./a.out # 执行可执行程序
然后,终端会有以下信息显示:
1 2 3 4 5 6 7 8 9 10 This is 0 thread create,ret=0,tid=140107547932224 This is 1 new thread.tid=140107547932224 This is 1 thread create,ret=0,tid=140107539539520 This is 2 thread create,ret=0,tid=140107531146816 This is 3 new thread.tid=140107531146816 This is 3 new thread.tid=140107539539520 This is 3 thread create,ret=0,tid=140107522754112 This is 4 new thread.tid=140107522754112 This is 4 thread create,ret=0,tid=140107514361408 This is 5 new thread.tid=140107514361408
我们会发现,传入的到线程执行函数的参数与线程并不对应,这是为什么?原因就在于,线程创建后执行是不确定的,由于我们传入的是地址,创建出来的几个线程传入的参数都是这个地址中的数据,而线程创建后,可能并没有执行,就又创建了下一个线程,这就导致参数传递出现了问题。
2.1.3 使用实例3 由实例 2 的问题,我们可以想一下,线程执行顺序的不确定导致了问题的出现,那我创建后休眠一下,给线程运行的时间,让线程从不同的时间开始运行,是不是不就好了呢?,我们可以尝试一下:
点击查看实例3
test.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <stdio.h> #include <pthread.h> #include <unistd.h> void *testThreadParam (void *arg) ;int main (int argc, char *argv[]) { int ret; pthread_t tid[5 ]; int i = 0 ; for (i = 0 ; i < 5 ; i++) { ret = pthread_create(&tid[i], NULL , testThreadParam, (void *)&i); printf ("This is %d thread create,ret=%d,tid=%lu\n" , i, ret, tid[i]); sleep(1 ); } sleep(1 ); return 0 ; } void *testThreadParam (void *arg) { printf ("This is %d new thread.tid=%lu\n" , *(int *)arg, pthread_self()); return NULL ; }
在终端执行以下命令编译程序:
1 2 gcc test.c -Wall -l pthread # 生成可执行文件 a.out ./a.out # 执行可执行程序
然后,终端会有以下信息显示:
1 2 3 4 5 6 7 8 9 10 This is 0 thread create,ret=0,tid=139647833339456 This is 0 new thread.tid=139647833339456 This is 1 thread create,ret=0,tid=139647824946752 This is 1 new thread.tid=139647824946752 This is 2 thread create,ret=0,tid=139647816554048 This is 2 new thread.tid=139647816554048 This is 3 thread create,ret=0,tid=139647808161344 This is 3 new thread.tid=139647808161344 This is 4 thread create,ret=0,tid=139647799768640 This is 4 new thread.tid=139647799768640
可以发现,这样得出的参数是正确的,但是我们等待程序完成也等待了很久,无疑是很浪费时间的。
2.2 值传递 2.2.1 应该怎么写? 1 2 3 4 5 6 7 8 int *testThread (void *arg) ;pthread_create(&tid, NULL , (void *)testThread, NULL ); int pthread_create (pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg) ;
在上边的 pthread_create 函数中,我们知道传入的是一个地址,可是一想,地址不也是数值嘛?难道就不能把一个数字强行当做一个地址,然后直接访问这个转换过的地址,不也可以吗。那就来尝试一下吧。若是定义的变量为整型,那么定义变量→传入参数→使用参数的格式解读如下:
1 2 3 4 5 6 7 8 int arg = 1 ;(void *)arg (int )arg
【注意】 这样做会导致指针长度不匹配问题,有可能会导致数据出错,这就需要我们自己来把握了。上边的变量定义为 int 类型,而在 64 位平台下,指针是占据 8 字节,长度长于 int 类型,这样是不会有问题的,但是如果使用了比指针还要长的数据长度时,就要注意这个问题了,防止数据丢失。
2.2.1 使用实例1 点击查看实例1
test.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <stdio.h> #include <pthread.h> #include <unistd.h> void *testThreadParam1 (void *arg) ;int main (int argc, char *argv[]) { int ret; pthread_t tid; int arg = 5 ; ret = pthread_create(&tid, NULL , testThreadParam1, (void *)arg); printf ("This is main thread,ret=%d,tid=%lu\n" , ret, tid); sleep(1 ); return 0 ; } void *testThreadParam1 (void *arg) { printf ("This is a new thread test! pid=%d,tid=%lu\n" , getpid(), pthread_self()); printf ("input arg=%d\n" , (int )arg); printf ("after pthread exit\n" ); pthread_exit(NULL ); }
在终端执行以下命令编译程序:
1 gcc test.c -Wall -l pthread # 生成可执行文件 a.out
没啥大问题的话,应该会出现下边的警告提示:
1 2 3 4 5 6 7 8 9 test.c: In function ‘main’: test.c:13:53: warning: cast to pointer from integer of different size [-Wint-to-pointer-cast] 13 | thread_create(&tid, NULL, testThreadParam1, (void *)arg); | ^ test.c: In function ‘testThreadParam1’: test.c:24:27: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast] 24 | printf("input arg=%d\n", (int)arg); |
原因是什么呢?上边其实已经解释过了,这是强制转化导致的数据所占字节数量不一致导致的,我们自己把握好保证数据不出错就可以啦。然后执行可执行程序:
然后,终端会有以下信息显示:
1 2 3 4 This is main thread,ret=0,tid=139766219667008 This is a new thread test! pid=4816,tid=139766219667008 input arg=5 after pthread exit
通过上边的例子,我们会发现,除了一个警告外,参数的传递没有问题。
2.2.2 使用实例2 那么我们再来试一下创建 5 个子线程,并将参数传递进去,我们来看一看会不会产生地址传递的问题。
点击查看实例2
test.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <stdio.h> #include <pthread.h> #include <unistd.h> void *testThreadParam (void *arg) ;int main (int argc, char *argv[]) { int ret; pthread_t tid[5 ]; int i = 0 ; for (i = 0 ; i < 5 ; i++) { ret = pthread_create(&tid[i], NULL , testThreadParam, (void *)i); printf ("This is %d thread create,ret=%d,tid=%lu\n" , i, ret, tid[i]); } sleep(1 ); return 0 ; } void *testThreadParam (void *arg) { printf ("This is %d new thread.tid=%lu\n" , (int )arg, pthread_self()); return NULL ; }
在终端执行以下命令编译程序:
1 2 gcc test.c -Wall -l pthread # 生成可执行文件 a.out 依然会有警告,我们自己把握好就可以 ./a.out # 执行可执行程序
然后,终端会有以下信息显示:
1 2 3 4 5 6 7 8 9 10 This is 0 thread create,ret=0,tid=139813761599040 This is 1 thread create,ret=0,tid=139813753206336 This is 0 new thread.tid=139813761599040 This is 2 thread create,ret=0,tid=139813744813632 This is 3 thread create,ret=0,tid=139813736420928 This is 4 thread create,ret=0,tid=139813728028224 This is 2 new thread.tid=139813744813632 This is 1 new thread.tid=139813753206336 This is 3 new thread.tid=139813736420928 This is 4 new thread.tid=139813728028224
我们会发现,传入的到线程执行函数的参数与线程是对应的,值是没有问题的,并且我们并没有控制线程的执行顺序。这在后边可能会有很大的用处。
3. 可以创建多少线程? 3.1 影响因素? 我们可以通过进程来创建线程,那么,一个进程最多可以创建多少线程呢?在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址空间的范围也不同。
进程创建线程时,线程的最大数量与以下两个部分有关:
进程的虚拟内存空间上限 ,因为创建一个线程,操作系统需要为其分配一个栈空间,如果线程数量越多,所需的栈空间就要越大,那么虚拟内存就会占用的越多。
系统参数限制 ,虽然 Linux 并没有内核参数来控制单个进程创建的最大线程个数,但是有系统级别的参数来控制整个系统的最大线程个数。
我们先来看一看创建一个线程,系统默认分配的虚拟内存大小吧,我们可以在终端输入以下命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 core file size (blocks, -c) unlimited data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 7862 max locked memory (kbytes, -l) 64 max memory size (kbytes, -m) unlimited open files (-n) 1024 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 7862 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 real-time non-blocking time (microseconds, -R) unlimited core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 15308 max locked memory (kbytes, -l) 498501 max memory size (kbytes, -m) unlimited open files (-n) 1024 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 15308 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited
3.2 32 位系统 在 32 位系统下,按照每个线程 10MB 的大小创建线程,共有 3GB 可用虚拟存,这样一共大概是 300 个线程左右。我们可以写一个测试程序,测试一下 32 位系统下究竟可以创建多少线程。
点击查看实例
test.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <stdio.h> #include <pthread.h> #include <unistd.h> #include <string.h> #include <errno.h> void *testThread (void *arg) { while (1 ) { sleep(1 ); } return (void *)0 ; } int main (int argc, char *argv[]) { int err = 0 ; int count = 0 ; pthread_t tid; while (err == 0 ) { err = pthread_create(&tid, NULL , testThread, NULL ); count++; } printf ("create thread error: %s\n" , strerror(errno)); printf ("Maxmum number of thread within a process is %d\n" , count); getchar(); return 0 ; }
在终端执行以下命令编译程序:
1 2 gcc test.c -Wall -l pthread # 生成可执行文件 a.out ./a.out # 执行可执行程序
然后,终端会有以下信息显示:
1 2 create thread error: Cannot allocate memory Maxmum number of thread within a process is 384
【结论】 32 位系统,用户态的虚拟空间只有 3G ,如果创建线程时分配的栈空间是 10M ,那么一个进程最多只能创建 300 个左右的线程。
3.3 64 位系统 我使用的测试系统是 Ubuntu21.04 的 64 位版本,在虚拟机中运行,分配了 2 个单核 CPU ,分配的内存为 4GB 。 64 位系统意味着用户空间的虚拟内存最大值是 128TB ,这个数值是很大的,如果按创建一个线程需占用 10M 栈空间的情况来算,那么理论上可以创建 128TB/10MB 个线程,也就是 1000 多万个线程。
但是肯定创建不了那么多线程,除了虚拟内存的限制,还有系统的限制。比如下面这三个内核参数的大小,都会影响创建线程的上限:
/proc/sys/kernel/threads-max ,表示系统支持的最大线程数,默认值是 14553 ;
/proc/sys/kernel/pid_max ,表示系统全局的 PID 号数值的限制,每一个进程或线程都有 ID , ID 的值超过这个数,进程或线程就会创建失败,默认值是 32768 ;
proc/sys/vm/max_map_count ,表示限制一个进程可以拥有的 VMA (虚拟内存区域)的数量,具体什么意思呢,我也不是很清楚啦,反正如果它的值很小,也会导致创建线程失败,默认值是 65530 。
点击查看实例
test.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <stdio.h> #include <pthread.h> #include <unistd.h> #include <string.h> #include <errno.h> void *testThread (void *arg) { while (1 ) { sleep(1 ); } return (void *)0 ; } int main (int argc, char *argv[]) { int err = 0 ; int count = 0 ; pthread_t tid; while (err == 0 ) { err = pthread_create(&tid, NULL , testThread, NULL ); count++; } printf ("create thread error: %s\n" , strerror(errno)); printf ("Maxmum number of thread within a process is %d\n" , count); getchar(); return 0 ; }
在终端执行以下命令编译程序:
1 2 gcc test.c -Wall -l pthread # 生成可执行文件 a.out ./a.out # 执行可执行程序
然后,终端会有以下信息显示:
1 2 create thread error: Resource temporarily unavailable Maxmum number of thread within a process is 9859
可以发现,最多创建了 9859 个,并且,此时重新开一个终端的话,是无法开启的。
【结论】 64 位系统,用户态的虚拟空间大到有 128T ,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。
二、线程终止 1. 终止线程的方式 终止线程一般有如下方式:
(1)线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码; return 的返回值可以通过 pthread_join() 来获取的。
(2)线程调用 pthread_exit() 函数;
(3)调用 pthread_cancel() 取消线程
【注意】 如果进程中的任意线程调用 exit() 、 _exit() 或者 _Exit() ,那么将会导致整个进程终止。
2. 终止函数 2.1 pthread_exit() 2.1.1 函数说明 在 linux 下可以使用 man pthread_create 命令查看该函数的帮助手册。
1 2 3 #include <pthread.h> void pthread_exit (void *retval) ;
【函数说明】 该函数将终止调用它的线程。
【函数参数】
retval : void * 类型,指定了线程的返回值(并非是函数的返回值)、也就是线程的退出码,这个退出码可以是数值,也可以是字符串。并且该退出码可由另一个线程通过调用 pthread_join() 来获取;同理,如果线程是在 start 函数中执行 return 语句终止,那么 return 的返回值也是可以通过 pthread_join() 来获取的。
【返回值】 none
【使用格式】 一般情况下基本使用格式如下:
1 2 3 4 5 6 7 #include <pthread.h> pthread_exit((void *)0 ); pthread_exit(NULL );
【注意事项】
(1)若需要设置退出码,且退出码为数值时,要注意做数据类型转换。
(2)调用 pthread_exit() 相当于在线程的 start 函数中执行 return 语句,不同之处在于,可在线程 start 函数所调用的任意函数中调用 pthread_exit() 来终止线程。如果主线程调用了 pthread_exit() ,那么主线程也会终止,但其它线程依然正常运行,直到进程中的所有线程终止才会使得进程终止。
2.1.2 使用实例 点击查看实例
test.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <stdio.h> #include <pthread.h> #include <unistd.h> void *testThreadExit (void *arg) ;int main (int argc, char *argv[]) { int ret; pthread_t tid; ret = pthread_create(&tid, NULL , testThreadExit, NULL ); printf ("This is main thread,ret=%d,tid=%lu\n" , ret, tid); sleep(1 ); printf ("main pthread exit!\n" ); pthread_exit(NULL ); return 0 ; } void * testThreadExit (void *arg) { printf ("This is a new thread test!\n" ); sleep(2 ); printf ("new pthread exit!\n" ); pthread_exit(NULL ); }
在终端执行以下命令编译程序:
1 2 gcc test.c -Wall -l pthread # 生成可执行文件 a.out ./a.out # 执行可执行程序
然后,终端会有以下信息显示:
1 2 3 4 This is main thread,ret=0,tid=140561027761728 This is a new thread test! main pthread exit! new pthread exit!
新线程中调用 sleep() 休眠,保证主线程先调用 pthread_exit() 终止,休眠结束之后新线程也调用 pthread_exit() 终止,根据打印结果,主线程调用 pthread_exit() 终止之后,整个进程并没有结束,而新线程还在继续运行。
三、线程回收 1. 僵尸线程 若线程并未分离( detached,啥意思?后边会学习到 ),则必须使用 pthread_join() 来等待线程终止,回收线程资源,这是因为如果线程终止后,其它线程没有调用 pthread_join() 函数来回收该线程,那么该线程将变成僵尸线程 ,与僵尸进程的概念相类似。同样,僵尸线程除了浪费系统资源外,若僵尸线程积累过多,那么会导致应用程序无法创建新的线程。
不过如果进程中存在着僵尸线程并未得到回收,当进程终止之后,进程会被其父进程回收,所以僵尸线程同样也会被回收。
2. 回收函数 2.1 pthread_join() 2.1.1 函数说明 在 linux 下可以使用 man pthread_join 命令查看该函数的帮助手册。
1 2 3 #include <pthread.h> int pthread_join (pthread_t thread, void **retval) ;
【函数说明】 该函数将阻塞等待线程的终止,并获取线程的退出码,回收线程资源。
【函数参数】
thread : pthread_t 类型, pthread_join() 等待指定线程的终止,通过参数 thread (线程 ID )指定需要等待的线程。
retval :如果参数 retval 不为 NULL ,则 pthread_join() 将目标线程的退出状态(即目标线程通过 pthread_exit() 退出时指定的返回值或者在线程 start 函数中执行 return 语句对应的返回值)复制到 *retval 所指向的内存区域;如果目标线程被 pthread_cancel() 取消,则将 PTHREAD_CANCELED 放在 *retval 中。如果对目标线程的终止状态不感兴趣,则可将参数 retval 设置为 NULL 。
【返回值】 int 类型,成功返回 0 ,失败时返回错误码。
【使用格式】 一般情况下基本使用格式如下:
1 2 3 4 5 6 #include <pthread.h> void *retv;pthread_join(tid, *retval);
【注意事项】 调用 pthread_join() 函数将会以阻塞的形式等待指定的线程终止,如果该线程已经终止,则 pthread_join() 立刻返回。如果多个线程同时尝试调用 pthread_join() 等待指定线程的终止,那么结果将是不确定的。
2.1.2 使用实例 点击查看实例
test.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <stdio.h> #include <pthread.h> #include <unistd.h> void *threadJoin (void *arg) ;int main (int argc, char *argv[]) { int ret; pthread_t tid[5 ]; void *retv; int i; for (i = 0 ; i < 5 ; i++) { ret = pthread_create(&tid[i], NULL , threadJoin, (void *)i); printf ("This is [%d] thread create,ret=%d,tid=%lu\n" , i, ret, tid[i]); } for (i = 0 ; i < 5 ; i++) { pthread_join(tid[i],&retv); printf ("thread[%d] retv=%s\n" , i, (char *)retv); } sleep(1 ); return 0 ; } void *threadJoin (void *arg) { printf ("This is [%d] thread test! pid=%d,tid=%lu\n" ,(int )arg, getpid(), pthread_self()); sleep(1 ); pthread_exit("thread return!" ); }
在终端执行以下命令编译程序:
1 2 gcc test.c -Wall -l pthread # 生成可执行文件 a.out 依然会有警告,我们自己把握好就可以 ./a.out # 执行可执行程序
然后,终端会有以下信息显示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 This is [0] thread create,ret=0,tid=140469036594752 This is [1] thread create,ret=0,tid=140469028202048 This is [2] thread create,ret=0,tid=140469019809344 This is [3] thread create,ret=0,tid=140469011416640 This is [4] thread create,ret=0,tid=140469003023936 This is [1] thread test! pid=3458,tid=140469028202048 This is [0] thread test! pid=3458,tid=140469036594752 This is [2] thread test! pid=3458,tid=140469019809344 This is [3] thread test! pid=3458,tid=140469011416640 This is [4] thread test! pid=3458,tid=140469003023936 thread[0] retv=thread return! thread[1] retv=thread return! thread[2] retv=thread return! thread[3] retv=thread return! thread[4] retv=thread return!
3. 内存演示 僵尸进程是可以直接使用 ps 命令看到的,是否真的被回收是很直观的,但是线程不同,是否真的被回收,我们好像是看不到的,不过我们可以通过 top 命令来查看我们进程所占用的内存资源,当结束的线程被回收后,应当是内存资源占用变小,若未回收已结束的线程,则内存资源应当不发生变化才对。
点击查看实例
通过宏 pthreadJoin 是否定义来选择是否编译线程回收部分程序,以达到测试的目的。
test.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <stdio.h> #include <pthread.h> #include <unistd.h> #define pthreadJoin void *threadJoin (void *arg) ;int main (int argc, char *argv[]) { int ret; pthread_t tid[5 ]; void *retv; int i; printf ("This main:pid=%d\n" , getpid()); for (i = 0 ; i < 100 ; i++) { ret = pthread_create(&tid[i], NULL , threadJoin, (void *)i); printf ("This is [%d] thread create,ret=%d,tid=%lu\n" , i, ret, tid[i]); } #ifdef pthreadJoin for (i = 0 ; i < 100 ; i++) { pthread_join(tid[i],&retv); printf ("thread[%d] retv=%s\n" , i, (char *)retv); } #endif while (1 ) { sleep(1 ); } return 0 ; } void *threadJoin (void *arg) { printf ("This is [%d] thread test! pid=%d,tid=%lu\n" ,(int )arg, getpid(), pthread_self()); sleep(25 ); pthread_exit("thread return!" ); }
在终端执行以下命令编译程序:
1 2 gcc test.c -Wall -l pthread # 生成可执行文件 a.out 依然会有警告,我们自己把握好就可以 ./a.out # 执行可执行程序
然后,终端会有以下信息显示:
1 2 3 4 5 This is [0] thread,ret=0,tid=139887072605760 This is [0] thread test! pid=6543,tid=139887072605760 This is [1] thread,ret=0,tid=139887064213056 This is [2] thread,ret=0,tid=139887055820352 # 其他的省略 ...
这里我们只关注 pid 的值即可,因为这是进程的 PID 然后重新开一个终端输入以下命令:
然后便会进入动态查看该进程变化情况的界面,显示信息如下:
1 2 3 4 5 6 7 8 top - 16:07:36 up8: 09,1 user, load average: 0.00,0.00,0.00 任务:1 total,0 running, 1 sleeping, 0 stopped, 0 zombie % Cpu(s): 0.3 us,0.3 Sy,0.0 ni, 99.3 id ,0.0 wa,0.0 hi,0.0 si,0.0 st MiB Mem : 3894.5 total, 1620.9 free, 1188.6 used, 1085.1 buff/cache MiB Swap : 1162.4total, 1162.4 free, 0.0 used. 2410.2 avail Mem 进程号 USER PR NI VIRT RES SHR %CPU %MEM TIME+ COMMAND 6874 hk 20 0 101076 1904 1724 S 0.0 0.0 0:00.00 a.out
然后我们关注 VIRT 一栏的信息即可。其实差别还是特别明显的。
4. 与进程回收的区别 通过上面的介绍可知, pthread_join() 执行的功能类似于针对进程的 waitpid() 调用,不过二者之间存在一些显著差别:
(1)线程之间关系是对等的。进程中的任意线程均可调用 pthread_join() 函数来等待另一个线程的终止。例如,如果线程 A 创建了线程 B ,线程 B 再创建线程 C ,那么线程 A 可以调用 pthread_join() 等待线程 C 的终止,线程 C 也可以调用 pthread_join() 等待线程 A 的终止;这与进程间层次关系不同,父进程如果使用 fork() 创建了子进程,那么它也是唯一能够对子进程调用 wait() 的进程,线程之间不存在这样的关系。
()不能以非阻塞的方式调用 pthread_join() 。对于进程,调用 waitpid() 既可以实现阻塞方式等待、也可以实现非阻塞方式等待。