LV05-02-线程-05-线程取消与清理

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

点击查看使用工具及版本
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_cancel()

1.1.1 函数说明

在 linux 下可以使用 man pthread_cancel 命令查看该函数的帮助手册。

1
2
3
/* Compile and link with -pthread. */
#include <pthread.h>
int pthread_cancel(pthread_t thread);

【函数说明】该函数向一个指定的线程(包括调用此函数的线程)发送取消请求。

【函数参数】

  • thread : pthread_t 类型,参数 thread 指定需要取消的目标线程的线程 ID 。

【返回值】 int 类型,成功返回 0 ,失败将返回错误码。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <pthread.h>

/* 至少应该有的语句 */
pthread_cancel(tid);
pthread_cancel(pthread_self());/* 向自己发送请求*/

【注意事项】

(1)发出取消请求之后,函数 pthread_cancel() 立即返回,不会等待目标线程的退出。默认情况下,目标线程也会立刻退出,其行为表现为如同调用了参数为 PTHREAD_CANCELED (其实就是 (void *)-1 )的 pthread_exit() 函数,但是,线程可以设置自己不被取消或者控制如何被取消后边会介绍,所以 pthread_cancel() 并不会等待线程终止,仅仅只是提出请求

(2)线程也可以向自己发送取消请求。

1
pthread_cancel(pthread_self());

1.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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <pthread.h>/* pthread_create pthread_exit pthread_cancel */
#include <unistd.h> /* sleep */
#include <string.h> /* strerror */

#define macro 1 /* 决定主线程是否发出取消请求 */

void *threadCancel(void *arg);

int main(int argc, char *argv[])
{
int i = 0;
int ret;
pthread_t tid;
void *retv;
/* 1. 创建一个线程 */
ret = pthread_create(&tid, NULL, (void *)threadCancel, NULL);
printf("This is main thread,ret=%d,tid=%lu\n", ret, tid);
for(i = 0; i < 2; i++)
{
sleep(1);/* 停留2s后取消创建的新线程*/
printf("main thread running!i = %d\n", i);
}
/* 2. 取消线程 */
printf("cancel thread!\n----------------\n");
#if macro == 1
pthread_cancel(tid);
#endif
/* 3. 回收线程 */
ret = pthread_join(tid, &retv);
if(ret != 0)
printf("pthread_join error: %s\n", strerror(ret));
/* 这一句将会导致一个段错误 */
printf("thread ret=%s\n", (char*)retv);
return 0;
}

void *threadCancel(void *arg)
{
int i = 0;
printf("This is a new thread!\n");
for(i = 0; i < 5; i++)
{
printf("new thread running! i = %d\n", i);
sleep(1);
}

/* 由于线程取消,下边这句永远不会执行,所以就无返回值,于是主线程中的回收线程
函数就会出现内存访问的错误 */
printf("new thread exit!\n");
pthread_exit("new thread return");
}

程序中的宏 macro 用于决定主线程是否发出取消请求。

  • 若 macro 为 1 ,则主线程不会发出线程取消请求。
  • 若 macro 为 0 ,则主线程进入 for 循环休眠 2s 后发出取消请求。

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall -l pthread # 生成可执行文件 a.out 
./a.out # 执行可执行程序

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

1
2
3
4
5
6
7
8
9
10
This is main thread,ret=0,tid=140496116229696
This is a new thread!
new thread running! i = 0
main thread running!i = 0
new thread running! i = 1
main thread running!i = 1
cancel thread!
----------------
new thread running! i = 2
段错误 (核心已转储)

此种情况下,主线程在休眠 2s 后,向新线程发送取消请求,当新线程执行完循环内的 printf 后直接取消线程,所以后边的 pthread_exit() 函数是不会执行的,也就不存在退出码,这也意味着主线程中的线程回收函数 pthread_join() 根本就接收不到 retval 参数,而这又是一个指针变量,这个指针就成为了一个空指针,主线程中访问了一个空指针,自然会出现段错误。

在终端执行以下命令编译程序:

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
This is main thread,ret=0,tid=140382870578752
This is a new thread!
new thread running! i = 0
main thread running!i = 0
new thread running! i = 1
main thread running!i = 1
cancel thread!
----------------
new thread running! i = 2
new thread running! i = 3
new thread running! i = 4
new thread exit!
thread ret=new thread return

此种情况为正常运行,主线程和新线程都正常运行完毕。

【注意事项】

首先我们需要知道 printf 和 sleep 都属于 Cancellation points 也就是取消点,这个后边会有一节笔记专门记录这个这个的存在导致新线程中 sleep 语句和 printf 语句放置位置的不同就会有不同的情况,若 sleep 在前,当发出取消请求后, printf 不会再执行。

2. 取消状态及类型

2.1 pthread_setcancelstate()

2.1.1 函数说明

在 linux 下可以使用 man pthread_setcancelstate 命令查看该函数的帮助手册。

1
2
3
/* Compile and link with -pthread. */
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);

【函数说明】设置调用该函数的线程的取消状态。

【函数参数】

  • state : int 类型,参数 state 中给定的值将会被设置为调用线程的取消状态。
点击查看 state 可取的值
PTHREAD_CANCEL_ENABLE 线程可以取消,这是新创建的线程取消性状态的默认值,所以新建线程以及主线程默认都是可以取消的。
PTHREAD_CANCEL_DISABLE线程不可被取消,如果此类线程接收到取消请求,则会将请求挂起,直至线程的取消性状态变为PTHREAD_CANCEL_ENABLE。
  • oldstate : int * 类型,线程之前的取消性状态会保存在参数 oldstate 指向的缓冲区中,如果对之前的状态不感兴趣, Linux 允许将参数 oldstate 设置为 NULL 。

【返回值】 int 类型,成功返回 0 ,失败将返回错误码。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <pthread.h>

/* 至少应该有的语句 */
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);/* 设置为不可被取消 */
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);/* 设置为可被取消 */

【注意事项】 pthread_setcancelstate() 函数执行的设置取消性状态和获取旧状态操作,这两步是一个原子操作。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <pthread.h>/* pthread_create pthread_exit pthread_cancel */
#include <unistd.h> /* sleep */
#include <string.h> /* strerror */

#define macro 1 /* 决定新线程中是否设置取消状态 */

void *threadCancel(void *arg);

int main(int argc, char *argv[])
{
int i = 0;
int ret;
pthread_t tid;
void *retv;
/* 1. 创建一个线程 */
ret = pthread_create(&tid, NULL, (void *)threadCancel, NULL);
printf("This is main thread,ret=%d,tid=%lu\n", ret, tid);
for(i = 0; i < 2; i++)
{
sleep(1);/* 停留5s后取消创建的新线程*/
printf("main thread running!i = %d\n", i);
}
/* 2. 取消线程 */
printf("cancel thread!\n----------------\n");
pthread_cancel(tid);
/* 3. 回收线程 */
ret = pthread_join(tid, &retv);
if(ret != 0)
printf("pthread_join error: %s\n", strerror(ret));
/* 这一句将会导致一个段错误 */
printf("thread ret=%s\n", (char*)retv);
return 0;
}

void *threadCancel(void *arg)
{
int i = 0;
#if macro == 1
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);/* 设置为不可被取消 */
#endif
printf("This is a new thread!\n");
for(i = 0; i < 5; i++)
{
sleep(1);
printf("new thread running!i = %d\n", i);
}
#if macro == 1
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);/* 设置为可被取消 */
#endif
printf("new thread exit!\n");
pthread_exit("new thread return");
}

程序中主线程进入 for 循环休眠 2s 后发出取消请求,宏 macro 用于决定新线程中是否设置取消状态。

  • 若 macro 为 1 ,则新线程会设置为不可取消, for 循环执行完毕后,线程才正常结束。
  • 若 macro 为 0 ,则主线程进入 for 循环休眠 2s 后发出取消请求。

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall -l pthread # 生成可执行文件 a.out 
./a.out # 执行可执行程序

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

1
2
3
4
5
6
7
8
9
10
11
12
This is main thread,ret=0,tid=139966405953088
This is a new thread!
main thread running!i = 0
new thread running!i = 0
main thread running!i = 1
cancel thread!
----------------
new thread running!i = 1
new thread running!i = 2
new thread running!i = 3
new thread running!i = 4
段错误 (核心已转储)

此种情况下,主线程在休眠 2s 后,向新线程发送取消请求,由于新线程设置了自己不可被取消,所以会执行完自己内部的 for 循环,循环执行完毕后又设置自己为可取消,而下边刚好又出现了 printf 函数( Cancellation points ),此时线程直接取消,所以线程后边的线程退出函数 pthread_exit() 是不会执行的,也就不存在退出码,这也意味着主线程中的线程回收函数 pthread_join() 根本就接收不到 retval 参数,而这又是一个指针变量,这个指针就成为了一个空指针,主线程中访问了一个空指针,自然会出现段错误。

其实尝试一下去掉线程退出函数前边的 printf 语句,会发现线程会正常的退出,不会发生段错误。

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall -l pthread # 生成可执行文件 a.out 
./a.out # 执行可执行程序

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

1
2
3
4
5
6
7
8
9
This is main thread,ret=0,tid=140574419174976
This is a new thread!
main thread running!i = 0
new thread running!i = 0
main thread running!i = 1
cancel thread!
----------------
new thread running!i = 1
段错误 (核心已转储)

此种情况为主线程发送取消请求后,新线程中遇到 sleep 就直接取消了。

【注意事项】 printf 和 sleep 都属于 Cancellation points 也就是取消点。

2.2 pthread_setcanceltype()

2.2.1 函数说明

在 linux 下可以使用 man pthread_setcanceltype 命令查看该函数的帮助手册。

1
2
3
/* Compile and link with -pthread. */
#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);

【函数说明】设置调用该函数的线程的取消类型。

【函数参数】

  • type : int 类型,参数 type 中给定的值将会被设置为调用线程的取消类型。
点击查看 type 可取的值
PTHREAD_CANCEL_DEFERRED取消请求到来时,线程还是继续运行,取消请求被挂起,直到线程到达某个取消点(cancellation point)为止,这是所有新建线程包括主线程默认的取消性类型。
PTHREAD_CANCEL_ASYNCHRONOUS可能会在任何时间点取消线程,这种取消性类型应用场景很少。
【说明】通常是立即取消,但不一定,这里的立即只是说目标线程不用等执行到属于cancellation point的函数的时候才会取消,它会在获得调度之后立即取消,因为内核调度会有延时,所以并不能保证时间上的“立即”。
  • oldtype : int * 类型,线程之前的取消类型会保存在参数 oldtype 指向的缓冲区中,如果对之前的状态不感兴趣, Linux 允许将参数 oldtype 设置为 NULL 。

【返回值】 int 类型,成功返回 0 ,失败将返回错误码。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <pthread.h>

/* 至少应该有的语句 */
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);/* 到达某个取消点取消 */
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL);/* 立刻取消 */

【注意事项】 pthread_setcanceltype() 函数执行的设置取消性类型和获取旧类型操作,这两步是一个原子操作。

2.2.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
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <pthread.h>/* pthread_create pthread_exit pthread_cancel pthread_testcancel pthread_self*/
#include <unistd.h> /* sleep */
#include <string.h>

#define macro 0

void *threadCancelType(void *arg);

int main(int argc, char *argv[])
{
int ret;
int i = 0;
pthread_t tid;
void *retv;
/* 1. 创建一个线程 */
ret = pthread_create(&tid, NULL, (void *)threadCancelType, NULL);
printf("This is main thread,ret=%d,tid=%lu\n", ret, tid);
for(i = 0; i < 2; i++)
{
sleep(1);
printf("main thread!i=%d\n", i);
}
/* 2. 申请取消线程 */
printf("----------------\ncancel thread!\n----------------\n");
pthread_cancel(tid);
/* 3. 回收线程,若取消成功,这里将会返回错误,然后打印信息 */
ret = pthread_join(tid, &retv);
if(ret != 0)
printf("pthread_join error: %s\n", strerror(ret));
printf("thread test end, code=%ld\n", (long)retv);
return 0;
}

void *threadCancelType(void *arg)
{
printf("This is a thread test!\n");
#if macro == 1
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL);/* 到达取消点取消 */
#else
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL);/* 立即取消 */
#endif
while(1);

return (void *)0;
}

程序中的宏 macro 用于配置新线程的取消类型。

  • 若 macro 为 1 ,则新线程取消类型为 PTHREAD_CANCEL_DEFERRED 。
  • 若 macro 为 0 ,则新线程取消类型为 PTHREAD_CANCEL_ASYNCHRONOUS 。

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall -l pthread # 生成可执行文件 a.out 
./a.out # 执行可执行程序

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

1
2
3
4
5
6
7
8
This is main thread,ret=0,tid=140369903380032
This is a thread test!
main thread!i=0
main thread!i=1
----------------
cancel thread!
----------------

此种情况下,主线程在休眠 2s 后,向新线程发送取消请求,新线程设置为了到达取消点取消,下边有一个 while(1) 的死循环,且内部无取消点,这就意味着新线程不会结束,主线程也会阻塞等待新线程结束。可以重开一个终端使用如下命令查看线程:

1
ps -eLf|grep a.out

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall -l pthread # 生成可执行文件 a.out 
./a.out # 执行可执行程序

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

1
2
3
4
5
6
7
8
This is main thread,ret=0,tid=140581044491840
This is a thread test!
main thread!i=0
main thread!i=1
----------------
cancel thread!
----------------
thread test end, code=-1

此种情况在新线程中设置了线程立即取消,所以即便没有遇到取消点,也会正常的结束线程,主线程也会正常执行完毕。

3. 取消点

3.1 取消点的概念

终于到了这一部分的笔记了,前边的关于线程取消的函数都是与取消点相关,现在就来了解一下这到底是什么吧。

前边知道若将线程的取消性类型设置为 PTHREAD_CANCEL_DEFERRED 时(线程可以取消状态下),收到其它线程发送过来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用。

所谓取消点其实就是一系列函数,当执行到这些函数的时候,才会真正响应取消请求,这些函数就是取消点;在没有出现取消点时,取消请求是无法得到处理的。这是因为系统认为,即便收到了取消请求,但是没有到达取消点时,线程正在执行的工作是不能被停止的,正在执行关键代码,此时终止线程将可能会导致出现意想不到的异常发生。

3.2 取消点函数

关于取消点函数,我们在 Linux 下可以使用 man 7 pthreads 进行查看,取消点函数主要是帮助手册的 Cancellation points 部分。下边仅仅是罗列写这篇笔记时, Ubuntu21.04 中 man 手册上显示的取消点函数,只是便于自己查看,最好还是直接查 man 手册吧( POSIX.1-2001 and/or POSIX.1-2008 )。

  • 在 POSIX.1-2001 and/or POSIX.1-2008 中必须是取消点的函数
点击查看函数列表
accept() getpmsg() open() putmsg() sem_wait() system()
aio_suspend() lockf() openat() putpmsg() send() tcdrain()
clock_nanosleep()mq_receive() pause() pwrite() sendmsg() usleep()
close() mq_send() poll() read() sendto() wait()
connect() mq_timedreceive()pread() readv() sigpause() waitid()
creat() mq_timedsend() pselect() recv() sigsuspend() waitpid()
fcntl() msgrcv() pthread_cond_timedwait()recvfrom() sigtimedwait()write()
fdatasync() msgsnd() pthread_cond_wait() recvmsg() sigwait() writev()
fsync() msync() pthread_join() select() sigwaitinfo()
getmsg() nanosleep() pthread_testcancel() sem_timedwait() sleep()

【说明】

(1)以下函数有备注,上边为了不占篇幅,将备注删去了

1
2
3
4
5
fcntl()    F_SETLKW
lockf() F_LOCK
openat() [Added in POSIX.1-2008]
sigpause() [POSIX.1-2001 only (moves to "may" list in POSIX.1-2008)]
usleep() [POSIX.1-2001 only (function removed in POSIX.1-2008)]

(2)还有一部分函数在 POSIX.1-2001 and/or POSIX.1-2008 可能是取消点函数,这些函数较多吗,这里就不写了,可以在 Ubuntu 中使用 man 7 pthreads 查看 Cancellation points 部分。

3.3 取消点函数执行嘛?

这个问题嘛,仅仅是记录自己的一个测试例程,上边我们知道 printf 函数就属于取消点函数,所以我们可以使用该函数进行一个测试。

点击查看实例
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
44
45
46
47
48
49
50
#include <stdio.h>
#include <pthread.h>/* pthread_create pthread_exit pthread_cancel pthread_testcancel pthread_self*/
#include <unistd.h> /* sleep */
#include <string.h>

#define macro 0

void *threadCancelType(void *arg);

int main(int argc, char *argv[])
{
int ret;
int i = 0;
pthread_t tid;
void *retv;
/* 1. 创建一个线程 */
ret = pthread_create(&tid, NULL, (void *)threadCancelType, NULL);
printf("This is main thread,ret=%d,tid=%lu\n", ret, tid);
for(i = 0; i < 2; i++)
{
sleep(1);
printf("main thread!i=%d\n", i);
}
/* 2. 申请取消线程 */
printf("----------------\ncancel thread!\n----------------\n");
pthread_cancel(tid);
/* 3. 回收线程,若取消成功,这里将会返回错误,然后打印信息 */
ret = pthread_join(tid, &retv);
if(ret != 0)
printf("pthread_join error: %s\n", strerror(ret));
printf("thread test end, code=%ld\n", (long)retv);
return 0;
}

void *threadCancelType(void *arg)
{
int i = 0;
printf("This is a thread test!\n");
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL); /* 设置为到取消点取消 */
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);/* 设置为不可被取消 */
for(i = 0; i < 5; i++)
{
printf("new pthread!i=%d\n", i);
sleep(1);
}
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);/* 设置为可被取消 */
printf("I'm printf() Cancellation points!\n");

return (void *)0;
}

在终端执行以下命令编译程序:

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
This is main thread,ret=0,tid=139932233295424
This is a thread test!
new pthread!i=0
main thread!i=0
new pthread!i=1
new pthread!i=2
main thread!i=1
----------------
cancel thread!
----------------
new pthread!i=3
new pthread!i=4
I'm printf() Cancellation points!
thread test end, code=-1

这说明取消点函数是执行完毕后退出的。

3.4 添加取消点

假设线程执行的是一个不含取消点的循环(譬如 for 循环、 while 循环),那么这时线程永远也不会响应取消请求,也就意味着除了线程自己主动退出,其它线程将无法通过向它发送取消请求而终止它,,但实际需求是,该线程必须可以被其它线程通过发送取消请求的方式终止,那这个时候怎么办?此时可以使用 pthread_testcancel() ,该函数目的很简单,就是产生一个取消点,线程如果已有处于挂起状态的取消请求,那么只要调用该函数,线程就会随之终止。

3.4.1 pthread_testcancel()

3.4.1.1 函数说明

在 linux 下可以使用 man pthread_testcancel 命令查看该函数的帮助手册。

1
2
3
/* Compile and link with -pthread. */
#include <pthread.h>
void pthread_testcancel(void);

【函数说明】设置一个取消点。

【函数参数】 none

【返回值】 none

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
/* 需要包含的头文件 */
#include <pthread.h>

/* 至少应该有的语句 */
pthread_testcancel()

【注意事项】 none

3.4.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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <pthread.h>/* pthread_create pthread_exit pthread_cancel pthread_testcancel pthread_self*/
#include <unistd.h> /* sleep */
#include <string.h>

#define macro 0

void *threadCancelType(void *arg);

int main(int argc, char *argv[])
{
int ret;
int i = 0;
pthread_t tid;
void *retv;
/* 1. 创建一个线程 */
ret = pthread_create(&tid, NULL, (void *)threadCancelType, NULL);
printf("This is main thread,ret=%d,tid=%lu\n", ret, tid);
for(i = 0; i < 2; i++)
{
sleep(1);
printf("main thread!i=%d\n", i);
}
/* 2. 申请取消线程 */
printf("----------------\ncancel thread!\n----------------\n");
pthread_cancel(tid);
/* 3. 回收线程,若取消成功,这里将会返回错误,然后打印信息 */
ret = pthread_join(tid, &retv);
if(ret != 0)
printf("pthread_join error: %s\n", strerror(ret));
printf("thread test end, code=%ld\n", (long)retv);
return 0;
}

void *threadCancelType(void *arg)
{
int i = 0;
printf("This is a thread test!\n");
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL);/* 到达取消点取消 */
while(1)
{
/* 屏蔽 sleep printf两个函数的取消点功能 */
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);/* 设置为不可被取消 */
sleep(1);
printf("new thread!i=%d\n", i);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);/* 设置为可被取消 */
/* 设置取消点 */
if(i++ == 6 && macro == 1)
pthread_testcancel();

}
return (void *)0;
}

程序中的宏 macro 用于配置新线程的取消类型。

  • 若 macro 为 1 ,则新线程自己添加一个取消点。
  • 若 macro 为 0 ,则新线程不设置取消点。

在终端执行以下命令编译程序:

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 main thread,ret=0,tid=140083039794752
This is a thread test!
main thread!i=0
new thread!i=0
main thread!i=1
----------------
cancel thread!
----------------
new thread!i=1
new thread!i=2
new thread!i=3
new thread!i=4
new thread!i=5
new thread!i=6
thread test end, code=-1

这个时候,当 i = 6 的时候设置取消点,线程被取消。

在终端执行以下命令编译程序:

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
16
This is main thread,ret=0,tid=140136216446528
This is a thread test!
new thread!i=0
main thread!i=0
main thread!i=1
----------------
cancel thread!
----------------
new thread!i=1
new thread!i=2
new thread!i=3
new thread!i=4
new thread!i=5
new thread!i=6
new thread!i=7
new thread!i=8

此种情况中,新线程是无法被取消的。

二、线程清理

前边学习了 atexit() 函数,使用 atexit() 函数注册进程终止处理函数,当进程调用 exit() 退出时就会执行进程终止处理函数;其实,当线程退出时也可以这样做,当线程终止退出时,去执行这样的处理函数,我们把这个函数称为线程清理函数( thread cleanup handler )。

一个线程可以注册多个清理函数,这些清理函数记录在栈中,每个线程都可以拥有一个清理函数栈,栈是一种先进后出的数据结构,也就是说它们的执行顺序与注册(添加)顺序相反,当执行完所有清理函数后,线程终止。

【注意事项】同一个清理函数可以被注册多次,满足执行条件的话注册多少次就会执行多少次。

1.函数说明

1.1 pthread_cleanup_push()

在 linux 下可以使用 man pthread_cleanup_push 命令查看该函数的帮助手册。

1
2
3
/* Compile and link with -pthread. */
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);

【函数说明】该函数向清理函数栈中添加一个清理函数。

【函数参数】

  • routine :是一个函数指针变量指向一个 routine() 函数,其中 routine() 函数无返回值,且只有一个 void * 类型参数。
  • arg : void * 类型,当调用清理函数 routine() 时,将 arg 作为 routine() 函数的参数。

【返回值】 none

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <pthread.h>

/* 至少应该有的语句 */
pthread_cleanup_push(cleanup, NULL);
pthread_cleanup_pop(0);

【注意事项】必须和 pthread_cleanup_pop() 成对使用。

1.2 pthread_cleanup_pop()

在 linux 下可以使用 man pthread_cleanup_pop 命令查看该函数的帮助手册。

1
2
3
/* Compile and link with -pthread. */
#include <pthread.h>
void pthread_cleanup_pop(int execute);

【函数说明】该函数将清理函数栈中最顶层(也就是最后添加的函数,最后入栈)的函数移除。

【函数参数】

  • execute : int 类型,如果为 0 ,清理函数不会被调用,只是将清理函数栈中最顶层的函数移除;如果参数 execute 为非 0 ,则除了将清理函数栈中最顶层的函数移除之外,还会执行相应的清理函数。

【返回值】 none

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <pthread.h>

/* 至少应该有的语句 */
pthread_cleanup_push(cleanup, NULL);
pthread_cleanup_pop(0);

【注意事项】必须和 pthread_cleanup_pop() 成对使用。

2. 函数使用

2.1 函数实现方式

上边介绍完了那两个函数,接下来先踩一个坑,就是这俩函数为啥必须成对出现?为啥有几个 pthread_cleanup_push 就要有几个 pthread_cleanup_pop() 呢?

我们来查一下这两个函数的实现( pthread.h ):

点击查看实现方式
1
2
3
4
5
6
7
8
9
10
11
12
#  define pthread_cleanup_push(routine, arg) \
do { \
struct __pthread_cleanup_frame __clframe \
__attribute__ ((__cleanup__ (__pthread_cleanup_routine))) \
= { .__cancel_routine = (routine), .__cancel_arg = (arg), \
.__do_it = 1 };

/* Remove a cleanup handler installed by the matching pthread_cleanup_push.
If EXECUTE is non-zero, the handler function is called. */
# define pthread_cleanup_pop(execute) \
__clframe.__do_it = (execute); \
} while (0)

尽管上面我们将 pthread_cleanup_push() 和 pthread_cleanup_pop() 称之为函数,但它们是通过宏来实现,可展开为分别由 { 和 } 所包裹的语句序列,所以必须在与线程相同的作用域中以匹配对的形式使用,必须一一对应着来使用。

当我们只使用 pthread_cleanup_push() 函数时,程序可能会报以下错误:

1
error: expected declaration or statement at end of input

2.2 函数执行条件

线程清理函数执行的条件:

(1)线程调用 pthread_exit() 退出时。

(2)线程响应取消请求时。

(3)用非 0 参数调用 pthread_cleanup_pop() 。

除了以上三种情况之外,其它方式终止线程将不会执行线程清理函数,例如在某线程函数start中执行 return 语句退出时就不会执行清理函数。

2.3 pthread_exit() 触发清理函数实例

点击查看 pthread_exit() 触发清理函数实例
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
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <pthread.h>/* pthread_create pthread_exit pthread_cancel pthread_setcancelstate*/
#include <unistd.h> /* sleep */
#include <string.h>

void *threadCleanUp(void *arg);
void threadCleanUp1(void *arg);
void threadCleanUp2(void *arg);

int main(int argc, char *argv[])
{
int ret;
pthread_t tid;
void *retv;
/* 1. 创建一个线程 */
ret = pthread_create(&tid, NULL, threadCleanUp, NULL);
printf("This is main thread,ret=%d,tid=%lu\n", ret, tid);
sleep(1);
/* 2. 回收线程,若取消成功,这里将会返回错误,然后打印信息 */
ret = pthread_join(tid, &retv);
if(ret != 0)
printf("pthread_join error: %s\n", strerror(ret));
printf("thread test end, code=%ld\n", (long)retv);
return 0;
}

void *threadCleanUp(void *arg)
{
int i = 0;
pthread_cleanup_push(threadCleanUp1,"threadCleanUp1 runing!");
pthread_cleanup_push(threadCleanUp2,"threadCleanUp2 runing!");
printf("This is a new thread!\n");
for(i = 0; i < 5; i++)
{
printf("thread runing[%d]!\n", i);
sleep(1);
}printf("The new thread is ready to exit!\n");
pthread_exit("thread return");
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return (void *)0;
}

void threadCleanUp1(void *arg)
{
printf("threadCleanUp1,arg=%s\n", (char*)arg);
}

void threadCleanUp2(void *arg)
{
printf("threadCleanUp2,arg=%s\n", (char*)arg);
}

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall -l pthread # 生成可执行文件 a.out 
./a.out # 执行可执行程序

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

1
2
3
4
5
6
7
8
9
10
11
This is main thread,ret=0,tid=140032863184448
This is a new thread!
thread runing[0]!
thread runing[1]!
thread runing[2]!
thread runing[3]!
thread runing[4]!
The new thread is ready to exit!
threadCleanUp2,arg=threadCleanUp2 runing!
threadCleanUp1,arg=threadCleanUp1 runing!
thread test end, code=93830143500505

2.4 pthread_cancel() 触发清理函数实例

点击查看 pthread_cancel() 触发清理函数实例
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <pthread.h>/* pthread_create pthread_exit pthread_cancel pthread_setcancelstate*/
#include <unistd.h> /* sleep */
#include <string.h>

void *threadCleanUp(void *arg);
void threadCleanUp1(void *arg);
void threadCleanUp2(void *arg);

int main(int argc, char *argv[])
{
int ret;
int i = 0;
pthread_t tid;
void *retv;
/* 1. 创建一个线程 */
ret = pthread_create(&tid, NULL, threadCleanUp, NULL);
printf("This is main thread,ret=%d,tid=%lu\n", ret, tid);
for(i = 0; i < 2; i++)
{
printf("main thread!i=%d\n", i);
sleep(1);
}
/* 2. 申请取消线程 */
printf("----------------\ncancel thread!\n----------------\n");
pthread_cancel(tid);
/* 3. 回收线程,若取消成功,这里将会返回错误,然后打印信息 */
ret = pthread_join(tid, &retv);
if(ret != 0)
printf("pthread_join error: %s\n", strerror(ret));
printf("thread test end, code=%ld\n", (long)retv);
return 0;
}

void *threadCleanUp(void *arg)
{
int i = 0;
pthread_cleanup_push(threadCleanUp1,"threadCleanUp1 runing!");
pthread_cleanup_push(threadCleanUp2,"threadCleanUp2 runing!");
printf("This is a thread test!\n");
for(i = 0; i < 5; i++)
{
printf("thread runing[%d]!\n", i);
sleep(1);
}
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return (void *)0;
}

void threadCleanUp1(void *arg)
{
printf("threadCleanUp1,arg=%s\n",(char*)arg);
}

void threadCleanUp2(void *arg)
{
printf("threadCleanUp2,arg=%s\n",(char*)arg);
}

在终端执行以下命令编译程序:

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
This is main thread,ret=0,tid=139866019890752
main thread!i=0
This is a thread test!
thread runing[0]!
main thread!i=1
thread runing[1]!
----------------
cancel thread!
----------------
thread runing[2]!
threadCleanUp2,arg=threadCleanUp2 runing!
threadCleanUp1,arg=threadCleanUp1 runing!
thread test end, code=-1
点击查看 用非 0 参数调用 pthread_cleanup_pop() 触发清理函数实例
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
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <pthread.h>/* pthread_create pthread_exit pthread_cancel pthread_setcancelstate*/
#include <unistd.h> /* sleep */
#include <string.h>

void *threadCleanUp(void *arg);
void threadCleanUp1(void *arg);
void threadCleanUp2(void *arg);

int main(int argc, char *argv[])
{
int ret;
pthread_t tid;
void *retv;
/* 1. 创建一个线程 */
ret = pthread_create(&tid, NULL, threadCleanUp, NULL);
printf("This is main thread,ret=%d,tid=%lu\n", ret, tid);

sleep(2);/* 这里停留一下,创建的线程才有时间运行 */
/* 2. 回收线程,若取消成功,这里将会返回错误,然后打印信息 */
ret = pthread_join(tid, &retv);
if(ret != 0)
printf("pthread_join error: %s\n", strerror(ret));
printf("thread test end, code=%ld\n", (long)retv);
return 0;
}

void *threadCleanUp(void *arg)
{
int i = 0;
pthread_cleanup_push(threadCleanUp1,"threadCleanUp1 runing!");
pthread_cleanup_push(threadCleanUp2,"threadCleanUp2 runing!");
printf("This is a thread test!\n");
for(i = 0; i < 5; i++)
{
printf("thread runing[%d]!\n", i);
sleep(1);
}
pthread_cleanup_pop(0);
pthread_cleanup_pop(1);/* 只执行这个回调函数 */
return (void *)0;
}

void threadCleanUp1(void *arg)
{
printf("threadCleanUp1,arg=%s\n",(char*)arg);
}

void threadCleanUp2(void *arg)
{
printf("threadCleanUp2,arg=%s\n",(char*)arg);
}

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall -l pthread # 生成可执行文件 a.out 
./a.out # 执行可执行程序

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

1
2
3
4
5
6
7
8
9
This is main thread,ret=0,tid=140153667556928
This is a thread test!
thread runing[0]!
thread runing[1]!
thread runing[2]!
thread runing[3]!
thread runing[4]!
threadCleanUp1,arg=threadCleanUp1 runing!
thread test end, code=0