最近学习《Linux高级程序设计(第三版)》12章关于多线程时,产生了不少问题。其中最无法理解的是“线程取消”和pthread_cleanup_push()/pthread_cleanup_pop()的一起使用的内涵,书本也没给demo;在看了一些网上的讲解之后,大概整理了下,顺便做个笔记。
pthread_cancel()的说明:
其中线程取消有先决条件,依设置的参数而定:
pthread_cleanup_push()/pthread_cleanup_pop()的说明:
上面这段话的意思就是:线程在使用同步机制(如互斥锁,条件变量)的情况下,如果遇到了被cancel的情况,有可能出现资源无法释放的情况,导致独占锁等情况。而当出现了这种情况,那么pthread_cleanup_push()/pthread_cleanup_pop()就派上用场了。
上述demo大意是想使用互斥锁和条件变量来进行简单的同步通信:线程1先运行然后等待条件变量进入阻塞,5秒后会被主线程cancel;在第10秒线程2会运行,并唤醒线程1。咋一看结果应该是终端至少会打印“t2 will unlock...”,然而终端的情况却是没有打印(见下图),可以判断出产生了锁的独占。
为什么会这样呢???
原因:
以下援引ChinaUnix论坛博主“xiaqian369”的解释:
下面我们看 Linux 是如何实现取消点的。(其实这个准确点儿应该说是 GNU 取消点实现,因为 pthread 库是实现在 glibc 中的。) 我们现在在 Linux 下使用的 pthread 库其实被替换成了 NPTL,被包含在 glibc 库中。
以 pthread_cond_wait 为例,glibc-2.6/nptl/pthread_cond_wait.c 中:
145 /* Enable asynchronous cancellation. Required by the standard. */ 146 cbuffer.oldtype = __pthread_enable_asynccancel (); 147 148 /* Wait until woken by signal or broadcast. */ 149 lll_futex_wait (&cond->__data.__futex, futex_val); 150 151 /* Disable asynchronous cancellation. */ 152 __pthread_disable_asynccancel (cbuffer.oldtype);
我们可以看到,在线程进入等待之前,pthread_cond_wait 先将线程取消类型设置为异步取消(__pthread_enable_asynccancel),当线程被唤醒时,线程取消类型被修改回延迟取消 __pthread_disable_asynccancel 。
这就意味着,所有在 __pthread_enable_asynccancel 之前接收到的取消请求都会等待 __pthread_enable_asynccancel 执行之后进行处理,所有在 __pthread_disable_asynccancel 之前接收到的请求都会在 __pthread_disable_asynccancel 之前被处理,所以真正的 Cancellation Point 是在这两点之间的一段时间。
当 main 函数中调用 pthread_cancel 前,t1 已经进入了 pthread_cond_wait 函数并将自己列入等待条件的线程列表中(lll_futex_wait)。这个可以通过 GDB 在各个函数上设置断点来验证。
当 pthread_cancel 被调用时,t1线程仍在等待,取消请求发生在 __pthread_disable_asynccancel 前,所以会被立即响应。但是 pthread_cond_wait 注册了一个线程清理程序(glibc-2.6/nptl/pthread_cond_wait.c):
126 /* Before we block we enable cancellation. Therefore we have to 127 install a cancellation handler. */ 128 __pthread_cleanup_push (&buffer, __condvar_cleanup, &cbuffer);那么这个线程清理程序 __condvar_cleanup 干了什么事情呢?我们可以注意到在它的实现最后(glibc-2.6/nptl/pthread_cond_wait.c):
85 /* Get the mutex before returning unless asynchronous cancellation 86 is in effect. */ 87 __pthread_mutex_cond_lock (cbuffer->mutex); 88}哦,__condvar_cleanup 在最后将 mutex 重新锁上了,在这个案例中也就是把lock又锁上了。而这时候 t2还在休眠(sleep(10)),等它醒来时,lock将会永远被锁住,这就是为什么 t1 陷入无休止的阻塞中。至于为什么在最后会再次锁住,本人才疏学浅,只能暂且接受这个事实吧,等以后学习了线程同步的实现机制和源码再回答这个问题吧。。。
根据上个案例出现的问题,我们将同步机制和清理函数结合使用,以解决问题。
引入了pthread_cleanup_push()/pthread_cleanup_pop()函数,在线程被取消时pop出此处定义的cleanup()函数,并执行它。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #define ERROR(S) {fprintf(stderr,S);exit(EXIT_FAILURE);} struct condition { pthread_mutex_t lock; //定义互斥锁 pthread_cond_t cd; //定义条件变量 }cond; void *function_t1(void *arg); //线程处理函数 void *function_t2(void *arg); void init(struct condition *cond); //初始化互斥锁和条件变量 void destroy(struct condition *cond); //销毁互斥锁和条件变量 void cleanup(); //清理函数 int main(int argc,char *argv[]) { pthread_t t1,t2; init(&cond); if(pthread_create(&t1,NULL,function_t1,NULL)!=0) ERROR("CREATE FAILED...\n") if(pthread_create(&t2,NULL,function_t2,NULL)!=0) ERROR("CREATE FAILED...\n") sleep(5); pthread_cancel(t1); pthread_join(t1,NULL); pthread_join(t2,NULL); destroy(&cond); return EXIT_SUCCESS; } void init(struct condition *cond) { if(pthread_mutex_init(&cond->lock,NULL)!=0) ERROR("MUTEXT INIT FAILED\n") if(pthread_cond_init(&cond->cd,NULL)!=0) ERROR("CONDITION VARIABLE INIT FAILED\n") } void destroy(struct condition *cond) { if(pthread_mutex_destroy(&cond->lock)!=0) ERROR("MUTEX DESTROY FAILED\n") if(pthread_cond_destroy(&cond->cd)!=0) ERROR("CONDITION VARIABLE DESTROY FAILED\n") } void cleanup() { pthread_mutex_unlock(&cond.lock); //保证不发生独占 } void *function_t1(void *arg) { pthread_cleanup_push(cleanup,NULL); pthread_mutex_lock(&cond.lock); //上锁 pthread_cond_wait(&cond.cd,&cond.lock); //等待条件变量成立,等待时进入阻塞状态并解锁. printf("t1 will unlock...\n"); pthread_mutex_unlock(&cond.lock); //解锁 pthread_cleanup_pop(0); pthread_exit(0); } void *function_t2(void *arg) { pthread_cleanup_push(cleanup,NULL); sleep(10); pthread_mutex_lock(&cond.lock); //上锁 pthread_cond_broadcast(&cond.cd); //通知等待条件变量成立的所有线程,此处即为通知t1线程,通知完成即解锁 printf("t2 will unlock...\n"); pthread_mutex_unlock(&cond.lock); //解锁 pthread_cleanup_pop(0); pthread_exit(0); }
运行结果:
这个案例很好的演示了pthread_cleanup_xx()函数的作用和地位。