LV05-02-线程-02-线程的创建与回收

本文主要是进程——线程的创建与回收的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
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
/* Compile and link with -pthread. */
#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; /* 使用printf()打印数据的时候,格式应该使用lu。 */
ret = pthread_create(&tid, NULL, (void *)testThread, NULL);/* testThread 要有定义*/

【注意事项】

(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>/* pthread_create */
#include <unistd.h> /* sleep */

int *testThread(void *arg);

int main(int argc, char *argv[])
{
int ret;
pthread_t tid;
/* 1. 创建一个线程 */
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 *)类型的地址 */
(void *)&arg

/* 参数使用格式,强制转化为指向(int *)类型的地址,然后再取地址内的数据 */
*(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>/* pthread_create pthread_exit pthread_self */
#include <unistd.h> /* sleep */

void *testThreadParam1(void *arg);

int main(int argc, char *argv[])
{
int ret;
pthread_t tid;
int arg = 5; /* 要传入的参数 */
/* 1. 创建一个线程 */
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>/* pthread_create pthread_exit pthread_self */
#include <unistd.h> /* sleep */

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++)
{
/* 1. 创建一个线程 */
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>/* pthread_create pthread_exit pthread_self */
#include <unistd.h> /* sleep */

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++)
{
/* 1. 创建一个线程 */
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;

/* 参数传入格式,强行将 1 当做一个void类型的地址进行参数传入 */
(void *)arg

/* 参数使用格式,由于传参的时候转化为了void类型,所以使用的时候需要强制转化回来 */
(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>/* pthread_create pthread_exit pthread_self */
#include <unistd.h> /* sleep */

void *testThreadParam1(void *arg);

int main(int argc, char *argv[])
{
int ret;
pthread_t tid;
int arg = 5; /* 要传入的参数 */
/* 1. 创建一个线程 */
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
./a.out # 执行可执行程序

然后,终端会有以下信息显示:

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>/* pthread_create pthread_exit pthread_self */
#include <unistd.h> /* sleep */

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++)
{
/* 1. 创建一个线程 */
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 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址空间的范围也不同。

image-20220527154120122

进程创建线程时,线程的最大数量与以下两个部分有关:

  • 进程的虚拟内存空间上限,因为创建一个线程,操作系统需要为其分配一个栈空间,如果线程数量越多,所需的栈空间就要越大,那么虚拟内存就会占用的越多。
  • 系统参数限制,虽然 Linux 并没有内核参数来控制单个进程创建的最大线程个数,但是有系统级别的参数来控制整个系统的最大线程个数。

我们先来看一看创建一个线程,系统默认分配的虚拟内存大小吧,我们可以在终端输入以下命令:

1
ulimit -a
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>/* pthread_create pthread_exit pthread_cancel pthread_self */
#include <unistd.h> /* sleep */
#include <string.h> /* strerror */
#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>/* pthread_create pthread_exit pthread_cancel pthread_self */
#include <unistd.h> /* sleep */
#include <string.h> /* strerror */
#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
/* Compile and link with -pthread. */
#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>/* pthread_create pthread_exit*/
#include <unistd.h> /* sleep */

void *testThreadExit(void *arg);

int main(int argc, char *argv[])
{
int ret;
pthread_t tid;
/* 1. 创建一个线程 */
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 pthread_exit(void *retval); */
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
/* Compile and link with -pthread. */
#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);/* 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>/* pthread_create pthread_exit pthread_self pthread_join */
#include <unistd.h> /* sleep */

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++)
{
/* 1. 创建一个线程 */
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++)
{
/* 2. 回收线程 */
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>/* pthread_create pthread_exit pthread_self pthread_join */
#include <unistd.h> /* sleep */

#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++)
{
/* 1. 创建一个线程 */
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++)
{
/* 2. 回收线程 */
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
top -p 6543

然后便会进入动态查看该进程变化情况的界面,显示信息如下:

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() 既可以实现阻塞方式等待、也可以实现非阻塞方式等待。