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

相关文章

Linux线程之线程的创建、属性、回收、退出、取消方式

《Linux线程之线程的创建、属性、回收、退出、取消方式》文章总结了线程管理核心知识:线程号唯一、创建方式、属性设置(如分离状态与栈大小)、回收机制(join/detach)、退出方法(返回/pthr... 目录1. 线程号2. 线程的创建3. 线程属性4. 线程的回收5. 线程的退出6. 线程的取消7.

Linux下进程的CPU配置与线程绑定过程

《Linux下进程的CPU配置与线程绑定过程》本文介绍Linux系统中基于进程和线程的CPU配置方法,通过taskset命令和pthread库调整亲和力,将进程/线程绑定到特定CPU核心以优化资源分配... 目录1 基于进程的CPU配置1.1 对CPU亲和力的配置1.2 绑定进程到指定CPU核上运行2 基于

MySQL 多列 IN 查询之语法、性能与实战技巧(最新整理)

《MySQL多列IN查询之语法、性能与实战技巧(最新整理)》本文详解MySQL多列IN查询,对比传统OR写法,强调其简洁高效,适合批量匹配复合键,通过联合索引、分批次优化提升性能,兼容多种数据库... 目录一、基础语法:多列 IN 的两种写法1. 直接值列表2. 子查询二、对比传统 OR 的写法三、性能分析

Javaee多线程之进程和线程之间的区别和联系(最新整理)

《Javaee多线程之进程和线程之间的区别和联系(最新整理)》进程是资源分配单位,线程是调度执行单位,共享资源更高效,创建线程五种方式:继承Thread、Runnable接口、匿名类、lambda,r... 目录进程和线程进程线程进程和线程的区别创建线程的五种写法继承Thread,重写run实现Runnab

SpringBoot线程池配置使用示例详解

《SpringBoot线程池配置使用示例详解》SpringBoot集成@Async注解,支持线程池参数配置(核心数、队列容量、拒绝策略等)及生命周期管理,结合监控与任务装饰器,提升异步处理效率与系统... 目录一、核心特性二、添加依赖三、参数详解四、配置线程池五、应用实践代码说明拒绝策略(Rejected

Spring IoC 容器的使用详解(最新整理)

《SpringIoC容器的使用详解(最新整理)》文章介绍了Spring框架中的应用分层思想与IoC容器原理,通过分层解耦业务逻辑、数据访问等模块,IoC容器利用@Component注解管理Bean... 目录1. 应用分层2. IoC 的介绍3. IoC 容器的使用3.1. bean 的存储3.2. 方法注

MySQL 删除数据详解(最新整理)

《MySQL删除数据详解(最新整理)》:本文主要介绍MySQL删除数据的相关知识,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录一、前言二、mysql 中的三种删除方式1.DELETE语句✅ 基本语法: 示例:2.TRUNCATE语句✅ 基本语

Java 线程安全与 volatile与单例模式问题及解决方案

《Java线程安全与volatile与单例模式问题及解决方案》文章主要讲解线程安全问题的五个成因(调度随机、变量修改、非原子操作、内存可见性、指令重排序)及解决方案,强调使用volatile关键字... 目录什么是线程安全线程安全问题的产生与解决方案线程的调度是随机的多个线程对同一个变量进行修改线程的修改操

Python变量与数据类型全解析(最新整理)

《Python变量与数据类型全解析(最新整理)》文章介绍Python变量作为数据载体,命名需遵循字母数字下划线规则,不可数字开头,大小写敏感,避免关键字,本文给大家介绍Python变量与数据类型全解析... 目录1、变量变量命名规范python数据类型1、基本数据类型数值类型(Number):布尔类型(bo

SQL Server数据库死锁处理超详细攻略

《SQLServer数据库死锁处理超详细攻略》SQLServer作为主流数据库管理系统,在高并发场景下可能面临死锁问题,影响系统性能和稳定性,这篇文章主要给大家介绍了关于SQLServer数据库死... 目录一、引言二、查询 Sqlserver 中造成死锁的 SPID三、用内置函数查询执行信息1. sp_w