线程【1】-POSIX的线程取消点(Cancellation Point)的概念和理解

本文详细探讨了POSIX线程中的取消点概念,解释了pthread_cancel函数的工作原理以及如何在特定情况下导致线程死锁。通过示例代码展示了在访问临界资源时,如果没有正确使用清理函数,可能会引发独占锁的问题,从而造成线程阻塞。解决方案是利用pthread_cleanup_push和pthread_cleanup_pop确保在取消时正确解锁资源。
摘要由CSDN通过智能技术生成

前言

提示:解决以下问题:

  1. 什么是取消点?
  2. 取消线程的使用异常
  3. 使用线程取消如何导致独占锁的情况?
  4. 为什么使用了线程同步机制之后,一定要使用清理函数?

一、什么是取消点?

1.1 pthread_cancel 做了什么?

  1. 线程默认是可以被取消的。

  2. pthread_cancel 函数只是给线程发送一个取消请希望可以将线程终止。

    • 对接收取消请求的线程来说只是一种建议。
    • 接收到取消请求的线程可能会马上停止,也可能不会直到遇到一个取消点之后。

1.2 取消点是什么?

  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);
}

在这里插入图片描述

总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值