使用 pthread_cancel 引入的死锁问题

先来说一下 pthread_cancel 基本概念。

pthread_cancel 调用并不是强制终止线程,它只提出请求。
线程如何处理 cancel 信号则由目标线程自己决定,可以是忽略、可以是立即终止、或者继续运行至 Cancelation-point(取消点),由不同的 Cancelation 状态决定。

有几个与 pthread_cancel  相关的函数也要提及一下:

int pthread_setcancelstate(int state, int *oldstate)

设置本线程对 cancel 信号的处理,state 有两种值:PTHREAD_CANCEL_ENABLE(缺省)和 PTHREAD_CANCEL_DISABLE,分别表示收到信号后设为 CANCLED 状态和忽略 CANCEL 信号继续运行。

int pthread_setcanceltype(int type, int *oldtype)

设置本线程取消动作的执行时机,type 有两种取值:PTHREAD_CANCEL_DEFFERED 和 PTHREAD_CANCEL_ASYCHRONOUS,仅当 cancel 状态为 ENABLE 时有效,分别表示收到信号后继续运行至下一个取消点再退出和立即执行取消动作(退出)。

void pthread_testcancel(void)

当线程中不包含取消点,但是又需要取消点的地方需使用此函数创建一个取消点,以便在一个没有包含取消点的执行代码线程中响应取消请求.

当然,线程的取消点并非只有调用该函数来设定,系统中有些函数调用也具有取消点特性如:pthread_cond_wait,sigwait(2) 等等。具体的大家可以网络上查询。

了解了 pthread_cancel 的取消机制之后,进入 bug 分析环节。

源码如下:

 1 #include <pthread.h>
 2 #include "stdio.h"
 3 #include "stdlib.h"
 4 #include "unistd.h"
 5
 6 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
 7 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
 8
 9 void* testThreadOne(void* arg)
10 {
11   pthread_mutex_lock(&mutex);
12   puts("ThreadOne label 1.");
13   pthread_cond_wait(&cond, &mutex);
14   puts("ThreadOne label 2.");
15   pthread_mutex_unlock(&mutex);
16   puts("ThreadOne label 3.");
17   pthread_exit(NULL);
18 }
19
20 void* testThreadTwo(void* arg)
21 {
22   sleep(2);
23   puts("ThreadTwo label 1.");
24   pthread_mutex_lock(&mutex);
25   puts("ThreadTwo label 2.");
26   pthread_cond_broadcast(&cond);
27   pthread_mutex_unlock(&mutex);
28   puts("ThreadTwo label 3.");
29   pthread_exit(NULL);
30 }
31
32 int main()
33 {
34   pthread_t tid[2] = {0};
35
36   pthread_create(&tid[0], NULL, testThreadOne, NULL);
37   pthread_create(&tid[1], NULL, testThreadTwo, NULL);
38
39   sleep(1);
40   puts("Main thread label 1.");
41   pthread_cancel(tid[0]);
42
43   pthread_join(tid[0], NULL);
44   pthread_join(tid[1], NULL);
45   pthread_mutex_destroy(&mutex);
46   pthread_cond_destroy(&cond);
47
48   return 0;
49 }

先来编译运行此程序:

ThreadOne label 1.
Main thread label 1.
ThreadTwo label 1.

结果不尽人意,程序并没有退出,产生了死锁的问题。

结合打印我们可以分析出程序停在了线程二 pthread_mutex_lock(&mutex) 的位置。

我们可以大致的梳理下整个程序的运行流程:

两个线程创建后,主线程会睡眠1秒,由于线程二开始也是要睡眠,所以此时线程一取得了运行权,
它会先将 mutex 上锁,并输出 label 1 信息,wait 函数内部会先将 mutex 解锁,然后等待 cond 条件,暂时没有其它线程唤醒,所以线程一会阻塞在此处。

由于主线程的睡眠时间较短,所以会优先被唤醒继续执行,输出 main label 1,随后调用 pthread_cancel 函数向线程一发出退出请求,并阻塞在 join 处。
此时线程一的 cancel 请求处理处于“受理”的状态,并且恰巧处于请求点(wait 调用),所以线程一会正常的退出。

流程继续,线程二的睡眠时间到并取得了运行权,先是输出 label 1 信息,然后请求 lock mutex,问题来了,线程二会在此阻塞下去。主线程也阻塞在 join 处无法退出。原因是为什么呢?

仔细一想我们就可以得出答案,通过之前的知识储备,wait 在调用时其内部会先将 mutex 解锁,如果被条件唤醒的话,它的内部会再次将 mutex 上锁来占据资源。

其实我们通过查看 GLIBC 的源码就可以来证明一切,我在这里贴出 2.30 版本的部分源码:

 1 static __always_inline int
 2 __pthread_cond_wait_common (pthread_cond_t *cond, pthread_mutex_t *mutex,
 3   clockid_t clockid,
 4   const struct timespec *abstime)
 5 {
 6     ...
 7   err = __pthread_mutex_unlock_usercnt (mutex, 0);
 8     ...
 9   futex_wait_cancelable(cond->__data.__g_signals + g, 0, private)
10     --> oldtype = __pthread_enable_asynccancel ();
11     --> int err = lll_futex_timed_wait (futex_word, expected, NULL, private);
12     --> __pthread_disable_asynccancel (oldtype);
13     ...
14   err = __pthread_mutex_cond_lock (mutex);
15     ...
16 }

通过源码可知,wait 函数的入口和出口处分别会对 mutex 进行加锁和解锁的操作,而在 __pthread_enable_asynccancel () 与 __pthread_disable_asynccancel (oldtype) 之间的时段里就对应着我们前面提到过的取消点,只有程序执行在两个函数之前时才可以被 cancel(默认状态下) 函数所取消。而我们使用 cancel 请求处于取消点的 wait 函数退出时,线程不是直接退出,而是将 wait 函数执行完成,所以BUG就这样引入了,mutex 并没有得到释放,可我们一定要这样的使用 cancel 函数的话,就没有解决锁释放的方法了么?

答案是有的,官方早已想到了这点,为我们精心准备了 pthread_cleanup_push 函数,它的作用就是在一些情况下退出线程做出一些收尾的动作,如使用 phread_exit、pthread_cancel 函数退出线程,在网上有说过线程异常退出的也可以调用 clean 函数,可笔者尝试过内存越界访问情况的异常,clean 函数却没有被调用,可能是指的不是这种情况吧。

利用 clean 函数,我们可以对前面的源程序中的线程一做一些修改,如下所示:

 1 void cleanup(void *arg)
 2 {
 3     pthread_mutex_unlock(&mutex);
 4 }
 5
 6 void *testThreadOne(void *arg)
 7 {
 8     pthread_cleanup_push(cleanup, NULL);
 9     pthread_mutex_lock(&mutex);
10     puts("ThreadOne label 1.");
11     pthread_cond_wait(&cond, &mutex);
12     puts("ThreadOne label 2.");
13     pthread_mutex_unlock(&mutex);
14     puts("ThreadOne label 3.");
15     pthread_cleanup_pop(0);
16     pthread_exit(NULL);
17 }

再次编译执行程序:

ThreadOne label 1.
Main thread label 1.
ThreadTwo label 1.
ThreadTwo label 2.
ThreadTwo label 3.

Perfect! mutex 得到了正确的释放,程序正常执行完毕。

本文参考自:https://www.cnblogs.com/mydomain/archive/2011/08/15/2139830.html

原文地址:https://www.cnblogs.com/GyForever1004/p/11455479.html

时间: 2024-07-29 10:41:05

使用 pthread_cancel 引入的死锁问题的相关文章

BT客户端实现 Peer协议设计

与peer建立tcp连接后,首先发送handshake消息进行握手 handshake消息格式如下: 一个字节0x19 + 一个字符串'BitTorrent protocol' + 8 byte 保留字节默认值为0(draft中对保留字节有定义) + 种子文件中info 部分的sha1字,大小为20个字节 + 20个自己的peer id(从tracker获取到的peer信息大多没有peerid,这个可以使用本地的peer id) 如果handshake信息协商不上,tcp连接将被关闭. BT标准

十一、java线程

目录: 一.线程的基本概念 二.线程的创建和启动 三.线程的调度和优先级 四.线程的状态控制 五.线程同步 一.线程的基本概念 线程是一个程序内部的顺序控制流 线程和进程的区别: 每个进程都由独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销 线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(pc),线程切换的开销小 多进程:在操作系统中能同时运行多个任务(程序) 多线程:在统一应用程序中有多个顺序流同时执行 java的线程是通过java.

07 | 行锁功过:怎么减少行锁对性能的影响?

在上一篇文章中,我跟你介绍了MySQL的全局锁和表级锁,今天我们就来讲讲MySQL的行锁. MySQL的行锁是在引擎层由各个引擎自己实现的.但并不是所有的引擎都支持行锁,比如MyISAM引擎就不支持行锁.不支持行锁意味着并发控制只能使用表锁,对于这种引擎的表,同一张表上任何时刻只能有一个更新在执行,这就会影响到业务并发度.InnoDB是支持行锁的,这也是MyISAM被InnoDB替代的重要原因之一. 我们今天就主要来聊聊InnoDB的行锁,以及如何通过减少锁冲突来提升业务并发度. 顾名思义,行锁

Linux环境下线程的同步与互斥以及死锁问题

由于本次要讨论操作系统的死锁问题,所以必须先研究的是linux环境下的线程同步与互斥 先看下面的代码 大家猜想输出应该是什么呢? 结果是下面这个样子 好吧,似乎并没有什么区别... 那么下面再看这段代码(请无视并忽略屏蔽的内容...) 大家猜想正确的结果是什么呢?5000,10000? 好吧,或许你们都错了. 在运行了一段时间后,它的结果是这样的. 是不是又对又错? 为什么呢? 这就是因为程序中printf语句作用:本身是库函数,所以必须进行系统调用,必须进入内核进行切换,有很大概率形成数据的混

线程的同步与互斥,死锁

线程的同步与互斥 多个线程同时访问共享数据时可能会发生冲突,比如两个线程同时把一个全局变量加1,结果可能不是我们所期待的: 我们看这段代码的执行结果: #include <stdio.h> #include <stdlib.h> #include <pthread.h> static int g_count=0; void *thread(void *arg) { int index=0; int tmp=0; while(index++<5000) { tmp=

12-24java面向对象之同步和死锁

案例1 设计一个线程操作类,要求可以产生三个线程对象,并可以设置三个线程的休眠时间 分析: 1.使用Thread类实现 class MyThread extends Thread { //封装属性 private String name ; //定义该线程的名称 private int time; //定义休眠时间 //构造方法 public MyThread(String name , int time) { super(name); this.time = time; } //覆写run方法

深入浅出 Java Concurrency (37): 并发总结 part 1 死锁与活跃度[转]

死锁与活跃度 前面谈了很多并发的特性和工具,但是大部分都是和锁有关的.我们使用锁来保证线程安全,但是这也会引起一些问题. 锁顺序死锁(lock-ordering deadlock):多个线程试图通过不同的顺序获得多个相同的资源,则发生的循环锁依赖现象. 动态的锁顺序死锁(Dynamic Lock Order Deadlocks):多个线程通过传递不同的锁造成的锁顺序死锁问题. 资源死锁(Resource Deadlocks):线程间相互等待对方持有的锁,并且谁都不会释放自己持有的锁发生的死锁.也

进程死锁及解决办法

操作系统 2009-09-24 16:48:58 阅读767 评论1   字号:大中小 订阅 一.要点提示 (1) 掌握死锁的概念和产生死锁的根本原因. (2) 理解产生死锁的必要条件--以下四个条件同时具备:互斥条件.不可抢占条件.占有且申请条件.循环等待条件. (3) 记住解决死锁的一般方法,掌握死锁的预防和死锁的避免二者的基本思想. (4) 掌握死锁的预防策略中资源有序分配策略. (5) 理解进程安全序列的概念,理解死锁与安全序列的关系. (6) 了解银行家算法. (7) 了解资源分配图.

Java之线程,常用方法,线程同步,死锁

1, 线程的概念 进程与线程 进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程.(进程是资源分配的最小单位) 线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小.(线程是cpu调度的最小单位) 切换而不是同步 一个程序中的方法有几条执行路径, 就有几个线程 Java中线程的生命周期 Java线程具有五中基本状态 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t =