文章目录
前言
提示:解决以下问题:
- 什么是取消点?
- 取消线程的使用异常
- 使用线程取消如何导致独占锁的情况?
- 为什么使用了线程同步机制之后,一定要使用清理函数?
一、什么是取消点?
1.1 pthread_cancel 做了什么?
-
线程默认是可以被取消的。
-
pthread_cancel 函数只是给线程发送一个取消请希望可以将线程终止。
- 对接收取消请求的线程来说只是一种建议。
- 接收到取消请求的线程可能会马上停止,也可能不会直到遇到一个取消点之后。
1.2 取消点是什么?
- 取消点是线程检测是否被取消的一个位置
pthread_join(3) \ pthread_cond_wait(3) pthread_cond_timedwait(3) pthread_testcancel(3) sem_wait(3) sigwait(3)
1.3 取消点的实现
下面我们看 Linux 是如何实现取消点的。(其实这个准确点儿应该说是 GNU 取消点实现,因为 pthread 库是实现在 glibc 中的。) 我们现在在 Linux 下使用的 pthread 库其实被替换成了 NPTL,被包含在 glibc 库中。
以 pthread_cond_wait 为例,glibc-2.6/nptl/pthread_cond_wait.c 中:
/* Enable asynchronous cancellation. Required by the standard. */
cbuffer.oldtype = __pthread_enable_asynccancel ();
/* Wait until woken by signal or broadcast. */
lll_futex_wait (&cond->__data.__futex, futex_val);
/* Disable asynchronous cancellation. */
__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 是在这两点之间的一段时间。
二 、取消线程的使用异常
2.1 异常出现的原因:
pthread_cleanup_push()/pthread_cleanup_pop()的说明
:
资源独占锁的使用:线程为了访问临界资源而为其加上锁,但是在访问过程中被外界取消。如果线程处于响应取消状态,且采用异步方式响应,或者在释放独占锁以前的路径上存在取消点,在临界资源将永远处于锁定状态得不到释放。外界取消操作是不可预见的。
2.2 取消线程引起的死锁
#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);
}
以上代码属于:在访问临界资源且在释放独占锁的路径上遇到了取消点,而出现线程2遭到阻塞,形成死锁。
上述demo大意是想使用互斥锁和条件变量来进行简单的同步通信:线程1先运行然后等待条件变量进入阻塞,5秒后会被主线程cancel
;在第10秒线程2会运行,并唤醒线程1。咋一看结果应该是终端至少会打印“t2 will unlock.
…”,然而终端的情况却是没有打印(见下图),可以判断出产生了锁的独占。
2.3 原因分析
当main函数调用pthread_cancel之前,t1已经进入pthread_cond_wait函数将自己列入等待条件的线程列表中,(lll_futex_wait).
当 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 陷入无休止的阻塞中。至于为什么在最后会再次锁住,本人才疏学浅,只能暂且接受这个事实吧,等以后学习了线程同步的实现机制和源码再回答这个问题吧。。。
2.4 同步机制和清理函数结合使用,解决问题
引入了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);
}