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

2023-10-19 01:18

本文主要是介绍6、一个 pthread_cancel 引起的线程死锁【整理转载】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

说明:本文由【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 进行解锁。

示例代码

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

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

[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中:

示例代码

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 函数前先注册一个线程处理函数:

示例代码

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

这篇关于6、一个 pthread_cancel 引起的线程死锁【整理转载】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/236326

相关文章

Java方法重载与重写之同名方法的双面魔法(最新整理)

《Java方法重载与重写之同名方法的双面魔法(最新整理)》文章介绍了Java中的方法重载Overloading和方法重写Overriding的区别联系,方法重载是指在同一个类中,允许存在多个方法名相同... 目录Java方法重载与重写:同名方法的双面魔法方法重载(Overloading):同门师兄弟的不同绝

JAVA线程的周期及调度机制详解

《JAVA线程的周期及调度机制详解》Java线程的生命周期包括NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING和TERMINATED,线程调度依赖操作系统,采用抢占... 目录Java线程的生命周期线程状态转换示例代码JAVA线程调度机制优先级设置示例注意事项JAVA线程

深入理解Redis线程模型的原理及使用

《深入理解Redis线程模型的原理及使用》Redis的线程模型整体还是多线程的,只是后台执行指令的核心线程是单线程的,整个线程模型可以理解为还是以单线程为主,基于这种单线程为主的线程模型,不同客户端的... 目录1 Redis是单线程www.chinasem.cn还是多线程2 Redis如何保证指令原子性2.

C++实现一个简易线程池的使用小结

《C++实现一个简易线程池的使用小结》在现代软件开发中,多线程编程已经成为提升程序性能的常见手段,本文主要介绍了C++实现一个简易线程池的使用小结,感兴趣的可以了解一下... 在现代软件开发中,多线程编程已经成为提升程序性能的常见手段。无论是处理大量 I/O 请求的服务器,还是进行 CPU 密集型计算的应用

JDK21对虚拟线程的几种用法实践指南

《JDK21对虚拟线程的几种用法实践指南》虚拟线程是Java中的一种轻量级线程,由JVM管理,特别适合于I/O密集型任务,:本文主要介绍JDK21对虚拟线程的几种用法,文中通过代码介绍的非常详细,... 目录一、参考官方文档二、什么是虚拟线程三、几种用法1、Thread.ofVirtual().start(

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

Java 线程池+分布式实现代码

《Java线程池+分布式实现代码》在Java开发中,池通过预先创建并管理一定数量的资源,避免频繁创建和销毁资源带来的性能开销,从而提高系统效率,:本文主要介绍Java线程池+分布式实现代码,需要... 目录1. 线程池1.1 自定义线程池实现1.1.1 线程池核心1.1.2 代码示例1.2 总结流程2. J

Java JUC并发集合详解之线程安全容器完全攻略

《JavaJUC并发集合详解之线程安全容器完全攻略》Java通过java.util.concurrent(JUC)包提供了一整套线程安全的并发容器,它们不仅是简单的同步包装,更是基于精妙并发算法构建... 目录一、为什么需要JUC并发集合?二、核心并发集合分类与详解三、选型指南:如何选择合适的并发容器?在多

Java中如何正确的停掉线程

《Java中如何正确的停掉线程》Java通过interrupt()通知线程停止而非强制,确保线程自主处理中断,避免数据损坏,线程池的shutdown()等待任务完成,shutdownNow()强制中断... 目录为什么不强制停止为什么 Java 不提供强制停止线程的能力呢?如何用interrupt停止线程s

python 线程池顺序执行的方法实现

《python线程池顺序执行的方法实现》在Python中,线程池默认是并发执行任务的,但若需要实现任务的顺序执行,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋... 目录方案一:强制单线程(伪顺序执行)方案二:按提交顺序获取结果方案三:任务间依赖控制方案四:队列顺序消