线程终止
线程终止
POSIX线程终止相关函数
//头文件
#include <pthread.h>
//API函数
int pthread_join(pthread_t thread, void **value_ptr); void pthread_exit(void *retval); int pthread_cancel(pthread_t thread); void pthread_cleanup_push(void (*routine)(void*), void *arg); void pthread_cleanup_pop(int execute);
线程终止方式
单个线程可以通过3种方式退出,可以在不终止整个进程的情况下,停止线程的控制流。
(1)线程可以直接从启动例程(也就是线程函数)中返回,即执行return语句,返回值是线程的退出码。
(2)线程可以被同一进程中的其他线程取消。即其他线程调用pthread_cancel()函数。
(3)线程函数本身调用pthread_exit()。函数返回线程退出后传出来的retval指针。
【说明】
1. pthread_exit()函数的参数retval是一个无类型指针,这与pthread_create函数中传给启动例程的单个参数类似。进程中的其他线程可以通过调用pthread_join函数来访问到这个指针。
2. 调用pthread_join()函数将一直阻塞,直到指定的线程(参数thread)终止,终止方式是上面所描述的3种方式。
(1) 如果线程简单地从启动例程中返回,即执行return语句,pthread_join函数的参数value_ptr就包含返回码。
(2) 如果线程被其他线程取消,pthread_join函数的参数value_ptr指向的内存单元就被设置为PTHREAD_CANCELED。
(3) 如果线程是调用pthread_exit()函数退出的,pthread_join函数的参数value_ptr将能获取到pthread_exit()函数返回的retval指针。
3. 如果对线程的返回值不感兴趣,可以将pthread_exit函数的retval置为NULL,这种情况下,pthread_join()函数仍可以等待指定的线程终止,但并不会获取到线程的终止状态。
实例1:获取已终止的线程的退出码。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <pthread.h> 4 5 void* thr_fn1(void *arg) 6 { 7 printf("thread1 returning***\n"); 8 return (void*)1; 9 } 10 11 void* thr_fn2(void *arg) 12 { 13 printf("thread2 exiting###\n"); 14 pthread_exit((void*)2); 15 } 16 17 int main(int argc, char *argv[]) 18 { 19 int err; 20 pthread_t tid1, tid2; 21 void *tret; 22 23 if((err=pthread_create(&tid1, NULL, thr_fn1, NULL) != 0)){ 24 printf("Error: can`t create thread1!\n"); 25 } 26 if((err=pthread_create(&tid2, NULL, thr_fn2, NULL) != 0)){ 27 printf("Error: can`t create thread2!\n"); 28 } 29 30 if((err=pthread_join(tid1, &tret)) != 0){ 31 printf("Error: can`t join with thread1!\n"); 32 } 33 printf("thread1 exit code: %d\n", (int*)tret); 34 if((err=pthread_join(tid2, &tret)) != 0){ 35 printf("Error: can`t join with thread2!\n"); 36 } 37 printf("thread2 exit code: %d\n", (int*)tret); 38 39 return 0; 40 }
View Code
## 运行结果:
thread1 returning***
thread2 exiting###
thread1 exit code: 1
thread2 exit code: 2
【分析】从运行结果可以看出,当一个线程通过调用pthread_exit退出或者简单地从启动例程中返回(return语句),进程中的其他线程可以通过调用pthread_join函数获得该进程的退出状态。
【说明】pthread_create和pthread_exit函数的无类型指针参数可以传递的值不止一个,这个指针可以是包含复杂信息的结构的地址,但是注意的是,这个结构指针指向的内存空间在调用者(线程函数)完成调用以后仍然是有效的。例如,在调用线程的栈区上分配了该结构,那么其他线程在使用这个结构时内存内容可能已经改变了。又如,线程在自己的栈区上分配了一个结构,然后把指向这个结构的指针传给pthread_exit,那么调用pthread_join的线程试图使用该结构时,这个栈区有可能已经被撤销,这块内存也已另作他用。
实例2:用局部(自动)变量(在栈区上分配的变量)作为pthread_exit的参数时出现的问题。
#include <stdio.h> #include <unistd.h> #include <pthread.h> typedef struct foo{ int a,b,c,d; }FOO; void printfoo(const char *s, const FOO *fp) { printf("%s", s); printf(" structure at %p\n", fp); printf(" foo.a = %d\n", fp->a); printf(" foo.b = %d\n", fp->b); printf(" foo.c = %d\n", fp->c); printf(" foo.d = %d\n", fp->d); } void* thr_fn1(void *arg) { FOO foo={1,2,3,4}; printf("thread 1: thread_id=%lu\n", pthread_self()); printfoo("thread 1:\n", &foo); printf("****** thread 1 exiting\n"); pthread_exit((void*)&foo); } void* thr_fn2(void *arg) { printf("thread 2: thread_id=%lu\n", pthread_self()); printf("###### thread 2 exiting\n"); pthread_exit((void*)2); } int main(int argc, char *argv[]) { int err; pthread_t tid1,tid2; FOO *fp; printf("Parent starting the first thread\n"); if((err=pthread_create(&tid1, NULL, thr_fn1, NULL) != 0)){ printf("Error: can`t create thread1!\n"); } if((err=pthread_join(tid1, (void*)&fp)) != 0){ printf("Error: can`t join with thread1!\n"); } sleep(1); printf("\nParent starting the second thread\n"); if((err=pthread_create(&tid2, NULL, thr_fn2, NULL) != 0)){ printf("Error: can`t create thread2!\n"); } sleep(1); printfoo("\nParent thread:\n", fp); return 0; }
View Code
## 运行结果:
Parent starting the first thread
thread 1: thread_id=140128023041792
thread 1:
structure at 0x7f7219094f00
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
****** thread 1 exiting
Parent starting the second thread
thread 2: thread_id=140128023041792
###### thread 2 exiting
Parent thread:
structure at 0x7f7219094f00
foo.a = 420042496
foo.b = 32626
foo.c = 1
foo.d = 0
【分析】从运行结果可以看出,当主线程访问局部结构时,结构的内容(在线程tid1的栈上分配的)已经发生改变了。即主线程试图访问已退出的tid1线程传给它的结构时,由于该结构是在线程tid1的栈区上定义的,当线程退出时,栈区的内存空间也随之释放掉了,所以读取到的内容是随机值。为了解决这个问题,可以使用动态内存分配(malloc)或者使用全局结构。
线程取消机制
在默认情况下,pthread_cancel()函数会使得thread标识的线程的行为表现为如同调用了参数为PTHREAD_CANCELED的pthread_exit()函数,即pthread_exit(PTHREAD_CANCELED)。但是,线程可以选择忽略取消或者控制如何被取消。【注意】pthread_cancel函数并不等待线程终止,它仅仅是提出请求。
实例3:线程取消的使用。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <pthread.h> 4 5 int done = 0; 6 int cnt = 0; 7 8 void* thr_fn(void *arg) 9 { 10 //printf("new thread start\n"); //线程取消点 11 while(!done){ 12 cnt++; 13 if(cnt == 10) 14 pthread_testcancel(); //自己设置一个线程取消点 15 } 16 return ((void*)1); 17 } 18 19 int main(int argc, char *argv[]) 20 { 21 int err; 22 pthread_t tid; 23 void *tret; 24 25 if(0 != (err=pthread_create(&tid, NULL, thr_fn, NULL))){ 26 printf("Error: can`t create thread\n"); 27 return -1; 28 } 29 pthread_cancel(tid); 30 if(0 != (err=pthread_join(tid, &tret))){ 31 printf("Error: can`t join with thread\n"); 32 return -2; 33 } 34 printf("thread exit code: %d\n", (int*)tret); 35 printf("cnt = %d\n", cnt); 36 37 return 0; 38 }
View Code
## 运行结果:
thread exit code: -1
cnt = 10
【分析】在主线程中调用了pthread_cancel(tid),在线程的启动例程中,当cnt==10时,调用了pthread_testcancel()函数,这个函数是表示设置一个函数取消点。当线程运行到取消点的时候,线程就会终止。线程退出时的状态码为-1,说明了线程的退出是非正常退出的,而正常退出是的状态码应该是1。
【说明】线程在收到pthread_cancel的取消请求后,可能会忽略、立即取消线程或者运行至取消点再取消线程。系统默认情况下,收到取消请求后,线程会继续运行,直到遇到下一个取消点处终止线程。
取消点:取消点是线程检查它是否被取消的一个点,posix保证在一些函数中会自带取消点,如sleep,accept,write,read,printf等,当执行上述函数时,自动触发线程取消点,使线程终止。
【扩展】实际上,线程是否取消除了与取消点有关外,还和线程的取消状态有关。取消状态分为:PTHREAD_CANCEL_ENABLE(可取消状态,这是系统默认的线程取消状态);PTHREAD_CANCEL_DISABLE(不可取消状态)。当线程的取消状态是PTHREAD_CANCEL_DISABLE时,即使线程收到取消请求在取消点也不会取消线程,直到可取消状态变更为PTHREAD_CANCEL_ENABLE时,线程才会在下一个取消点取消线程。
//设置线程取消点函数
void pthread_testcancel(void);
//修改线程的取消状态函数 int pthread_setcancelstate(int state, int *oldstate);
【参数说明】
state:设置新状态值。
oldstate:存放原先的取消状态。
【函数说明】该函数会在函数内部设置一个取消点,调用该函数时,如果收到一个取消请求,且取消状态是可取消的,就会立即将线程取消。如果取消状态为不可取消,且没有取消请求,就不会取消,直到两者条件都满足时才会取消函数。
在线程的属性中还有一个属性与线程的取消有关,即它的取消类型,之前我们所说的取消属于推迟取消,即在调用pthread_cancel函数后,需要等到线程运行至一个取消点时,线程才会被取消而终止线程。
但是,还有一种取类型为异步取消,即当调用pthread_cancel后,线程就会被立即取消,而不用等到线程运行至取消点时再取消线程,取消类型同取消状态一样可以修改。
//修改线程的取消类型函数 int pthread_setcanceltype(int type, int *oldtype);
【参数说明】
type:设置新的取消类型。
oldtype:存放原先的取消类型。
【函数说明】取消类型有:PTHREAD_CANCEL_DEFERRED、PTHREAD_CANCEL_ASYNCHRONOUS。
PTHREAD_CANCEL_DEFERRED:线程接收到取消请求后,直到运行至"取消点"后才取消线程。
PTHREAD_CANCEL_ASYNCHRONOUS:线程接收到取消请求后,立即取消线程。
<说明>线程的“取消状态”和“取消类型”存在于任意一个新建线程中,包括主线程,默认设置是PTHREAD_CANCEL_ENABLE 和 PTHREAD_CANCEL_DEFERRED。
线程清理处理程序
线程可以安排它退出时需要调用的函数,这与进程在退出时可以用atexit函数安排退出是类似的。这样的函数被称为线程处理清理程序(thread cleanup handler)。一个线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说它们的执行顺序与它们注册时相反。
//注册线程清理处理程序 void pthread_cleanup_push(void (*rtn)(void*), void *arg);
【参数】
rtn:线程退出时被调用的清理函数。
arg:传入给rtn的参数。 //解除线程清理处理程序 void pthread_cleanup_pop(int execute);
【说明】当线程执行以下动作时,清理函数rtn是由phtread_cleanup_push函数调度的,调用时只传入一个参数arg。
- 线程函数调用pthread_exit时;
- 响应取消线程请求时;
- 用非零execute参数调用pthread_cleanup_pop时。
<注意> 如果pthread_cleanup_pop的execute参数如果设置为0,清理函数rtn将不被调用,也就是说,线程函数执行pthread_cleanup_pop(0)时,在phtread_cleanup_push中注册的清理函数rtn将不被执行,但是
pthread_cleanup_pop函数仍将删除上次在phtread_cleanup_push函数中注册的清理处理程序(或函数)。
【扩展】这两个函数有一个限制,因为它们可以实现为宏,pthread_cleanup_push()与pthread_cleanup_pop()必须成对的出现在线程函数相同的作用域中。
pthread_cleanup_push的宏定义可以包含字符 { ,这种情况下,在与pthread_cleanup_pop的宏定义中要有对应的匹配字符 } 。示例如下:
#define pthread_cleanup_push(rtn,arg) { \ struct _pthread_handler_rec __cleanup_handler, **__head; \ __cleanup_handler.rtn = rtn; \ __cleanup_handler.arg = arg; \ (void) pthread_getspecific(_pthread_handler_key, &__head); \ __cleanup_handler.next = *__head; \ *__head = &__cleanup_handler; #define pthread_cleanup_pop(ex) \ *__head = __cleanup_handler.next; \ if (ex) (*__cleanup_handler.rtn)(__cleanup_handler.arg); \ }
如果pthread_cleanup_pop函数的参数execute设置为0,清理将不被调用。但不管发生上述哪种情况,pthread_cleanup_pop都将删除上次pthread_cleanup_push调用建立的清理处理程序。示例如下:
pthread_cleanup_push(routine, &arg); ...... pthread_cleanup_pop(0); pthread_exit((void*)1);
<说明>当线程函数执行到pthread_exit函数时,pthread_cleanup_pop函数将解除pthread_cleanup_push函数注册的清理处理函数routine,但是不会执行routine中的函数体代码。
实例4:使用线程清理处理程序。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <pthread.h> 4 5 void cleanup(void *arg) 6 { 7 printf("cleanup: %s\n", (char*)arg); 8 } 9 10 void* thr_fn1(void *arg) 11 { 12 printf("thread 1 start\n"); 13 pthread_cleanup_push(cleanup, "thread 1 first handler"); 14 pthread_cleanup_push(cleanup, "thread 1 secend handler"); 15 printf("thread 1 push complete\n"); 16 if(arg) 17 return ((void*)11); 18 pthread_cleanup_pop(0); 19 pthread_cleanup_pop(0); 20 return ((void*)12); 21 } 22 23 void* thr_fn2(void *arg) 24 { 25 printf("thread 2 start\n"); 26 pthread_cleanup_push(cleanup, "thread 2 first handler"); 27 pthread_cleanup_push(cleanup, "thread 2 secend handler"); 28 printf("thread 2 push complete\n"); 29 if(arg) 30 pthread_exit((void*)21); 31 pthread_cleanup_pop(0); 32 pthread_cleanup_pop(0); 33 pthread_exit((void*)22); 34 } 35 36 void* thr_fn3(void *arg) 37 { 38 printf("thread 3 start\n"); 39 pthread_cleanup_push(cleanup, "thread 3 first handler"); 40 pthread_cleanup_push(cleanup, "thread 3 secend handler"); 41 printf("thread 3 push complete\n"); 42 if(arg) 43 pthread_exit((void*)31); 44 pthread_cleanup_pop(0); 45 pthread_cleanup_pop(0); 46 pthread_exit((void*)32); 47 } 48 49 int main(int argc, char *argv[]) 50 { 51 int err; 52 pthread_t tid1, tid2, tid3; 53 void *tret; 54 55 if(0 != (err = pthread_create(&tid1, NULL, thr_fn1, (void*)1) )){ 56 printf("Error: can`t create thread 1\n"); 57 } 58 if(0 != (err = pthread_create(&tid2, NULL, thr_fn2, (void*)1) )){ 59 printf("Error: can`t create thread 2\n"); 60 } 61 if(0 != (err = pthread_create(&tid3, NULL, thr_fn3, NULL) )){ 62 printf("Error: can`t create thread 3\n"); 63 } 64 65 if(0 != (err = pthread_join(tid1, &tret))){ 66 printf("Error: can`t join with thread 1\n"); 67 } 68 printf("thread 1 exit code: %d\n", (int*)tret); 69 if(0 != (err = pthread_join(tid2, &tret))){ 70 printf("Error: can`t join with thread 2\n"); 71 } 72 printf("thread 2 exit code: %d\n", (int*)tret); 73 if(0 != (err = pthread_join(tid3, &tret))){ 74 printf("Error: can`t join with thread 3\n"); 75 } 76 printf("thread 3 exit code: %d\n", (int*)tret); 77 78 return 0; 79 }
View Code
## 运行结果:
thread 1 start
thread 1 push complete
thread 1 exit code: 11
thread 2 start
thread 2 push complete
cleanup: thread 2 secend handler
cleanup: thread 2 first handler
thread 3 start
thread 3 push complete
thread 2 exit code: 21
thread 3 exit code: 32
## 分析:
1、线程1是直接执行return语句终止线程的,即return ((void*)11); 没有执行到pthread_cleanup_pop(0); 线程就终止了,并没有执行在pthread_cleanup_push函数中注册的清理函数cleanup,因为它不满足注册的清理函数被调用的那3个条件中的任何一个,所以线程1的退出码为11,即thread 1 exit code: 11。
2、线程2是执行到pthread_exit((void*)21); 时线程就终止了,满足已注册的清理函数被调用的条件。这时将调用在pthread_cleanup_push中注册的清理函数cleanup。从运行结果中可以看到,调用顺序和注册顺序是相反的,这是因为清理函数是记录在栈中的,而栈是一种先进后出的数据结构。特别值得注意的是,
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
pthread_exit((void*)22);
在线程2的启动例程函数体中,上面的3条语句是没有执行到的,从线程2的退出码结果为:thread 2 exit code: 21 可以证明这一点。
3、线程3是执行到pthread_exit((void*)32); 时线程终止,并且在线程函数体中是执行了两个pthread_cleanup_pop(0); 语句的,所以pthread_cleanup_pop函数会删除掉在前面的pthread_cleanup_push中注册的清理函数cleanup,但是不会执行清理处理函数,线程3的退出码为:thread 3 exit code: 32。
综上所述,可以得出以下结论:
1、如果线程是通过从它的启动例程中调用return语句而终止的话,它的清理处理程序就不会被调用。
2、清理处理程序是按照与它们注册时相反的顺序被调用的。
进程和线程原语的比较
在默认状态下,线程的终止状态会保存直到对该线程调用pthread_join。但是如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。在线程被分离后,我们不能用pthread_join函数等待它的终止状态,因为对分离线程调用pthread_join会产生未定义的行为。分离线程可以调用pthread_detach()函数。
//线程分离函数 int pthread_detach(pthread_t thread);
【参数】thread:待分离的线程ID值。
【返回值】成功,返回0;失败,返回错误码。