关于Linux线程退出时清理函数及取消点的讨论(笔记)

mac2022-07-01  16

背景:

最近学习《Linux高级程序设计(第三版)》12章关于多线程时,产生了不少问题。其中最无法理解的是“线程取消”和pthread_cleanup_push()/pthread_cleanup_pop()的一起使用的内涵,书本也没给demo;在看了一些网上的讲解之后,大概整理了下,顺便做个笔记。

背景知识点:

pthread_cancel()的说明:

其中线程取消有先决条件,依设置的参数而定:

pthread_cleanup_push()/pthread_cleanup_pop()的说明:

上面这段话的意思就是:线程在使用同步机制(如互斥锁,条件变量)的情况下,如果遇到了被cancel的情况,有可能出现资源无法释放的情况,导致独占锁等情况。而当出现了这种情况,那么pthread_cleanup_push()/pthread_cleanup_pop()就派上用场了。

案例分析:

#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); //销毁互斥锁和条件变量 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 *function_t1(void *arg) { pthread_mutex_lock(&cond.lock); //上锁 pthread_cond_wait(&cond.cd,&cond.lock); //等待条件变量成立,等待时进入阻塞状态并解锁. printf("t1 will unlock...\n"); pthread_mutex_unlock(&cond.lock); //解锁 pthread_exit(0); } void *function_t2(void *arg) { sleep(10); pthread_mutex_lock(&cond.lock); //上锁 pthread_cond_broadcast(&cond.cd); //通知等待条件变量成立的所有线程,此处即为通知t1线程,通知完成即解锁 printf("t2 will unlock...\n"); pthread_mutex_unlock(&cond.lock); //解锁 pthread_exit(0); }

上述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()函数的作用和地位。

最新回复(0)