背景:
最近学习《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()函数的作用和地位。