6、一个 pthread_cancel 引起的线程死锁【整理转载】

简介: 说明:本文由【2,3】整理而得。 这篇文章主要从一个 Linux 下一个 pthread_cancel 函数引起的多线程死锁小例子出发来说明 Linux 系统对 POSIX 线程取消点的实现方式,以及如何避免因此产生的线程死锁。

说明:本文由【2,3】整理而得。

这篇文章主要从一个 Linux 下一个 pthread_cancel 函数引起的多线程死锁小例子出发来说明 Linux 系统对 POSIX 线程取消点的实现方式,以及如何避免因此产生的线程死锁。

目 录:

1. 一个 pthread_cancel 引起的线程死锁小例子

2. 取消点(Cancellation Point)

3. 取消类型(Cancellation Type)

4. Linux 的取消点实现

5. 对示例函数进入死锁的解释

6. 如何避免因此产生的死锁

7. 结论

8. 参考文献

1. 一个 pthread_cancel 引起的线程死锁小例子

下面是一段在Linux 平台下能引起线程死锁的小例子。这个实例程序仅仅是使用了条件变量和互斥量进行一个简单的线程同步,thread0 首先启动,锁住互斥量 mutex,然后调用 pthread_cond_wait,它将线程 tid[0] 放在等待条件的线程列表上后,对 mutex 解锁。thread1 启动后等待 10 秒钟,此时 pthread_cond_wait 应该已经将 mutex 解锁,这时 tid[1] 线程锁住 mutex,然后广播信号唤醒 cond 等待条件的所有等待线程,之后解锁 mutex。当 mutex 解锁后,tid[0] 线程的 pthread_cond_wait 函数重新锁住 mutex 并返回,最后 tid[0] 再对 mutex 进行解锁。

示例代码

img_1c53668bcee393edac0d7b3b3daff1ae.gif img_405b18b4b6584ae338e0f6ecaf736533.gif View Code
#include <pthread.h>
#include
"stdio.h"
#include
"stdlib.h"
#include
"unistd.h"

pthread_mutex_t mutex
= PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond
= PTHREAD_COND_INITIALIZER;

void* thread0(void* arg)
{
pthread_mutex_lock(
&mutex);
printf(
"in thread 0 tag 1\n");
pthread_cond_wait(
&cond, &mutex);
printf(
"in thread 0 tag 2\n");
pthread_mutex_unlock(
&mutex);
printf(
"in thread 0 tag 3\n");
pthread_exit(NULL);
}

void* thread1(void* arg)
{
sleep(
10);
printf(
"in thread 1 tag 1\n");
pthread_mutex_lock(
&mutex);
printf(
"in thread 1 tag 2\n");
pthread_cond_broadcast(
&cond);
pthread_mutex_unlock(
&mutex);
printf(
"in thread 1 tag 3\n");
pthread_exit(NULL);
}
int main()
{
pthread_t tid[
2];
if (pthread_create(&tid[0], NULL, thread0, NULL) != 0)
{
exit(
1);
}
if (pthread_create(&tid[1], NULL, thread1, NULL) != 0)
{
exit(
1);
}
sleep(
5);
printf(
"in main thread tag 1\n");
pthread_cancel(tid[
0]);

pthread_join(tid[
0], NULL);
pthread_join(tid[
1], NULL);

pthread_mutex_destroy(
&mutex);
pthread_cond_destroy(
&cond);
return 0;
}

示例代码_对上述程序的跟踪

img_1c53668bcee393edac0d7b3b3daff1ae.gif img_405b18b4b6584ae338e0f6ecaf736533.gif View Code
[Thread debugging using libthread_db enabled]

Breakpoint
8, main () at testthread.cpp:34
34 if (pthread_create(&tid[0], NULL, thread0, NULL) != 0)
(gdb) bt
#
0 main () at testthread.cpp:34
(gdb) n
[New Thread
0xb7fecb70 (LWP 2494)]
in thread 0 tag 1

Breakpoint
9, main () at testthread.cpp:38
38 if (pthread_create(&tid[1], NULL, thread1, NULL) != 0)
(gdb) bt
#
0 main () at testthread.cpp:38
(gdb) n
[Switching to Thread
0xb7fecb70 (LWP 2494)]

Breakpoint
1, thread0 (arg=0x0) at testthread.cpp:13
13 pthread_cond_wait(&cond, &mutex);
(gdb) n
[New Thread
0xb77ebb70 (LWP 2495)]
in main thread tag 1
[Switching to Thread
0xb7fee6d0 (LWP 2491)]

Breakpoint
10, main () at testthread.cpp:44
44 pthread_cancel(tid[0]);
(gdb) n
in thread 1 tag 1

Breakpoint
11, main () at testthread.cpp:46
46 pthread_join(tid[0], NULL);
(gdb) n
[Switching to Thread
0xb77ebb70 (LWP 2495)]

Breakpoint
2, thread1 (arg=0x0) at testthread.cpp:24
24 pthread_mutex_lock(&mutex);
(gdb) n
[Thread
0xb7fecb70 (LWP 2494) exited]
[Switching to Thread
0xb7fee6d0 (LWP 2491)]

Breakpoint
12, main () at testthread.cpp:47
47 pthread_join(tid[1], NULL);
(gdb) n
^C
Program received signal SIGINT, Interrupt.
0x00110416 in __kernel_vsyscall ()
(gdb) info
break
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048742 in thread0(void*)
at testthread.cpp:
13
breakpoint already hit
1 time
2 breakpoint keep y 0x080487a4 in thread1(void*)
at testthread.cpp:
24
breakpoint already hit
1 time
3 breakpoint keep y 0x08048762 in thread0(void*)
at testthread.cpp:
15
4 breakpoint keep y 0x0804877a in thread0(void*)
at testthread.cpp:
17
5 breakpoint keep y 0x080487bc in thread1(void*)
at testthread.cpp:
26
6 breakpoint keep y 0x080487c8 in thread1(void*)
at testthread.cpp:
27
7 breakpoint keep y 0x080487e0 in thread1(void*)
at testthread.cpp:
29
8 breakpoint keep y 0x080487f5 in main() at testthread.cpp:34
breakpoint already hit
1 time
9 breakpoint keep y 0x0804882e in main() at testthread.cpp:38
breakpoint already hit
1 time
10 breakpoint keep y 0x08048882 in main() at testthread.cpp:44
breakpoint already hit
1 time
---Type <return> to continue, or q <return> to quit---
11 breakpoint keep y 0x0804888e in main() at testthread.cpp:46
breakpoint already hit
1 time
12 breakpoint keep y 0x080488a2 in main() at testthread.cpp:47
breakpoint already hit
1 time
13 breakpoint keep y 0x080488b6 in main() at testthread.cpp:49
(gdb)

我们发现,

Breakpoint 12, main () at testthread.cpp:47

47 pthread_join(tid[1], NULL);

(gdb) n

^C

一直卡在这里。

看起来似乎没有什么问题,但是 main 函数调用了一个 pthread_cancel 来取消 tid[0] 线程。上面程序编译后运行时会发生无法终止情况,看起来像是 pthread_cancel tid[0] 取消时没有执行 pthread_mutex_unlock 函数,这样 mutex 就被永远锁住,线程 tid[1] 也陷入无休止的等待中。事实是这样吗?

2. 取消点(Cancellation Point)

要注意的是 pthread_cancel 调用并不等待线程终止,它只提出请求。线程在取消请求(pthread_cancel)发出后会继续运行,直到到达某个取消点(Cancellation Point)。取消点是线程检查是否被取消并按照请求进行动作的一个位置。pthread_cancel manual 说以下几个 POSIX 线程函数是取消点:

    pthread_join(3)

pthread_cond_wait(3)

pthread_cond_timedwait(3)

pthread_testcancel(3)

sem_wait(3)

sigwait(3)

以及read()write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作

在中间我们可以找到 pthread_cond_wait 就是取消点之一。

但是,令人迷惑不解的是,所有介绍 Cancellation Points 的文章都仅仅说,当线程被取消后,将继续运行到取消点并发生取消动作。但我们注意到上面例子中 pthread_cancel 前面 main 函数已经 sleep 5 秒,那么在 pthread_cancel 被调用时,thread0 到底运行到 pthread_cond_wait 没有?

如果 thread0 运行到了 pthread_cond_wait,那么照上面的说法,它应该继续运行到下一个取消点并发生取消动作,而后面并没有取消点,所以 thread0 应该运行到 pthread_exit 并结束,这时 mutex 就会被解锁,这样就不应该发生死锁啊。

说明:

从我的GDB中可以看出,运行到pthread_cond_wait这里后,就没有往下运行了。应该说,这是当前的取消点。

3. 取消类型(Cancellation Type)

我们会发现,通常的说法:某某函数是 Cancellation Points,这种方法是容易令人混淆的。因为函数的执行是一个时间过程,而不是一个时间点。其实真正的 Cancellation Points 只是在这些函数中 Cancellation Type 被修改为 PHREAD_CANCEL_ASYNCHRONOUS 和修改回 PTHREAD_CANCEL_DEFERRED 中间的一段时间。

POSIX 的取消类型有两种,一种是延迟取消(PTHREAD_CANCEL_DEFERRED),这是系统默认的取消类型,即在线程到达取消点之前,不会出现真正的取消;另外一种是异步取消(PHREAD_CANCEL_ASYNCHRONOUS),使用异步取消时,线程可以在任意时间取消。

4. Linux 的取消点实现

下面我们看 Linux 是如何实现取消点的。(其实这个准确点儿应该说是 GNU 取消点实现,因为 pthread 库是实现在 glibc 中的。) 我们现在在 Linux 下使用的 pthread 库其实被替换成了 NPTL,被包含在 glibc 库中。

以 pthread_cond_wait 为例,glibc-2.6/nptl/pthread_cond_wait.c 中:

示例代码

img_1c53668bcee393edac0d7b3b3daff1ae.gif img_405b18b4b6584ae338e0f6ecaf736533.gif View Code
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 是在这两点之间的一段时间。(也就是在__pthread_enable_asynccancel__pthread_disable_asynccancel间处理取消请求)

5. 对示例函数进入死锁的解释

当main函数中调用 pthread_cancel 前,thread0 已经进入了 pthread_cond_wait 函数并将自己列入等待条件的线程列表中(lll_futex_wait)。这个可以通过 GDB 在各个函数上设置断点来验证。

当 pthread_cancel 被调用时,tid[0] 线程仍在等待,取消请求发生在 __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 重新锁上了。而这时候 thread1 还在休眠(sleep(10)),等它醒来时,mutex 将会永远被锁住,这就是为什么 thread1 陷入无休止的阻塞中。

【可是为什么pthread_cond_wait要在最后上锁呢?】

6. 如何避免因此产生的死锁

由于线程清理函数 pthread_cleanup_push 使用的策略是先进后出(FILO),那么我们可以在 pthread_cond_wait 函数前先注册一个线程处理函数:

示例代码

img_1c53668bcee393edac0d7b3b3daff1ae.gif img_405b18b4b6584ae338e0f6ecaf736533.gif View Code
void cleanup(void *arg)
{
pthread_mutex_unlock(
&mutex);
}
void* thread0(void* arg)
{
pthread_cleanup_push(cleanup, NULL);
// thread cleanup handler
pthread_mutex_lock(&mutex);
pthread_cond_wait(
&cond, &mutex);
pthread_mutex_unlock(
&mutex);
pthread_cleanup_pop(
0);
pthread_exit(NULL);
}

这样,当线程被取消时,先执行 pthread_cond_wait 中注册的线程清理函数 __condvar_cleanup,将 mutex 锁上,再执行 thread0 中注册的线程处理函数 cleanup,将mutex解锁。这样就避免了死锁的发生。

7. 结论

多线程下的线程同步一直是一个让人很头痛的问题。POSIX 为了避免立即取消程序引起的资源占用问题而引入的 Cancellation Points 概念是一个非常好的设计,但是不合适的使用 pthread_cancel 仍然会引起线程同步的问题。了解POSIX 线程取消点在 Linux 下的实现更有助于理解它的机制和有利于更好的应用这个机制。

8. 参考文献

[1] W. Richard Stevens, Stephen A. Rago: Advanced Programming in the UNIX Environment, 2nd Edition.

[2] Linux Manpage

http://wzw19191.blog.163.com/blog/static/131135470200992610550684/

[3] http://hi.baidu.com/hackers365/blog/item/412d0f085c1fd18f0a7b8205.html

4http://blog.csdn.net/yanook/article/details/6589798

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
目录
相关文章
|
12月前
|
安全 Java 程序员
【多线程-从零开始-肆】线程安全、加锁和死锁
【多线程-从零开始-肆】线程安全、加锁和死锁
193 0
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
173 6
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(中)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
184 5
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(上)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
161 3
|
Arthas 监控 Java
深入解析与解决高并发下的线程池死锁问题
在高并发的互联网应用中,遇到线程池死锁问题导致响应延迟和超时。问题源于库存服务的悲观锁策略和线程池配置不当。通过以下方式解决:1) 采用乐观锁(如Spring Data JPA的@Version注解)替换悲观锁,减少线程等待;2) 动态调整线程池参数,如核心线程数、最大线程数和拒绝策略,以适应业务负载变化;3) 实施超时和重试机制,减少资源占用。这些改进提高了系统稳定性和用户体验。
560 2
Java多线程-死锁的出现和解决
死锁是指多线程程序中,两个或以上的线程在运行时因争夺资源而造成的一种僵局。每个线程都在等待其中一个线程释放资源,但由于所有线程都被阻塞,故无法继续执行,导致程序停滞。例如,两个线程各持有一把钥匙(资源),却都需要对方的钥匙才能继续,结果双方都无法前进。这种情况常因不当使用`synchronized`关键字引起,该关键字用于同步线程对特定对象的访问,确保同一时刻只有一个线程可执行特定代码块。要避免死锁,需确保不同时满足互斥、不剥夺、请求保持及循环等待四个条件。
122 0
|
Java 测试技术 PHP
父子任务使用不当线程池死锁怎么解决?
在Java多线程编程中,线程池有助于提升性能与资源利用效率,但若父子任务共用同一池,则可能诱发死锁。本文通过一个具体案例剖析此问题:在一个固定大小为2的线程池中,父任务直接调用`outerTask`,而`outerTask`再次使用同一线程池异步调用`innerTask`。理论上,任务应迅速完成,但实际上却超时未完成。经由`jstack`输出的线程调用栈分析发现,线程陷入等待状态,形成“死锁”。原因是子任务需待父任务完成,而父任务则需等待子任务执行完毕以释放线程,从而相互阻塞。此问题在测试环境中不易显现,常在生产环境下高并发时爆发,重启或扩容仅能暂时缓解。
224 0
|
Java
死锁是线程间争夺资源造成的无限等待现象,Java示例展示了两个线程各自持有资源并等待对方释放,导致死锁。`
【6月更文挑战第20天】死锁是线程间争夺资源造成的无限等待现象,Java示例展示了两个线程各自持有资源并等待对方释放,导致死锁。`volatile`保证变量的可见性和部分原子性,确保多线程环境中值的即时更新。与`synchronized`相比,`volatile`作用于单个变量,不保证原子操作,同步范围有限,但开销较小。`synchronized`提供更全面的内存语义,保证原子性和可见性,适用于复杂并发控制。
129 3
|
Java
在Java中,死锁是指两个或多个线程互相等待对方释放资源,从而导致所有线程都无法继续执行的情况。
【6月更文挑战第24天】在Java并发中,死锁是多线程互相等待资源导致的僵局。避免死锁的关键策略包括:防止锁嵌套,设定固定的加锁顺序,使用`tryLock`带超时,避免无限等待,减少锁的持有时间,利用高级同步工具如`java.util.concurrent`,以及实施死锁检测和恢复机制。通过这些方法,可以提升程序的并发安全性。
108 1
|
消息中间件 算法 Java
(十四)深入并发之线程、进程、纤程、协程、管程与死锁、活锁、锁饥饿详解
本文深入探讨了并发编程的关键概念和技术挑战。首先介绍了进程、线程、纤程、协程、管程等概念,强调了这些概念是如何随多核时代的到来而演变的,以满足高性能计算的需求。随后,文章详细解释了死锁、活锁与锁饥饿等问题,通过生动的例子帮助理解这些现象,并提供了预防和解决这些问题的方法。最后,通过一个具体的死锁示例代码展示了如何在实践中遇到并发问题,并提供了几种常用的工具和技术来诊断和解决这些问题。本文旨在为并发编程的实践者提供一个全面的理解框架,帮助他们在开发过程中更好地处理并发问题。
299 0