`

linux多线程之pthread_cancel结束线程(防止死锁)

阅读更多
linux多线程之pthread_cancel结束线程
摘要:
这篇文章主要从一个 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 进行解锁。
1  #include <pthread.h>
2
3  pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
4  pthread_cond_t   cond = PTHREAD_COND_INITIALIZER;
5
6  void* thread0(void* arg)
7  {
8    pthread_mutex_lock(&amp;mutex);
9    pthread_cond_wait(&amp;cond, &amp;mutex);
10   pthread_mutex_unlock(&amp;mutex);
11   pthread_exit(NULL);
12 }
13
14void* thread1(void* arg)
15 {
16   sleep(10);
17   pthread_mutex_lock(&amp;mutex);
18   pthread_cond_broadcast(&amp;cond);
19   pthread_mutex_unlock(&amp;mutex);
20   pthread_exit(NULL);
21 }
22int main()
23 {
24   pthread_t tid[2];
25   if (pthread_create(&amp;tid[0], NULL, &amp;thread0, NULL) != 0) {
26     exit(1);
27   }
28   if (pthread_create(&amp;tid[1], NULL, &amp;thread1, NULL) != 0) {
29     exit(1);
30   }
31   sleep(5);
32   pthread_cancel(tid[0]);
33
34   pthread_join(tid[0], NULL);
35   pthread_join(tid[1], NULL);
36
37   pthread_mutex_destroy(&amp;mutex);
38   pthread_cond_destroy(&amp;cond);
39   return0;
40 }
看起来似乎没有什么问题,但是 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)
在中间我们可以找到 pthread_cond_wait 就是取消点之一。
但是,令人迷惑不解的是,所有介绍 Cancellation Points 的文章都仅仅说,当线程被取消后,将继续运行到取消点并发生取消动作。但我们注意到上面例子中 pthread_cancel 前面 main 函数已经 sleep 了 5 秒,那么在 pthread_cancel 被调用时,thread0 到底运行到 pthread_cond_wait 没有?
如果 thread0 运行到了 pthread_cond_wait,那么照上面的说法,它应该继续运行到下一个取消点并发生取消动作,而后面并没有取消点,所以 thread0 应该运行到 pthread_exit 并结束,这时 mutex 就会被解锁,这样就不应该发生死锁啊。
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 中:
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 (&amp;cond-&gt;__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 是在这两点之间的一段时间。
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 (&amp;buffer, __condvar_cleanup, &amp;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-&gt;mutex);
88}
哦,__condvar_cleanup 在最后将 mutex 重新锁上了。而这时候 thread1 还在休眠(sleep(10)),等它醒来时,mutex 将会永远被锁住,这就是为什么 thread1 陷入无休止的阻塞中。
6. 如何避免因此产生的死锁
由于线程清理函数 pthread_cleanup_push 使用的策略是先进后出(FILO),那么我们可以在 pthread_cond_wait 函数前先注册一个线程处理函数:
void cleanup(void *arg)
{
pthread_mutex_unlock(&amp;mutex);
}
void* thread0(void* arg)
{
pthread_cleanup_push(cleanup, NULL);  // thread cleanup handler
pthread_mutex_lock(&amp;mutex);
pthread_cond_wait(&amp;cond, &amp;mutex);
pthread_mutex_unlock(&amp;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 下的实现更有助于理解它的机制和有利于更好的应用这个机制。
分享到:
评论

相关推荐

    Linux多线程编程_linux多线程_Linux多线程;应用笔记_columnc9g_

    在Linux多线程编程中,我们通常使用POSIX线程库(pthread),它提供了跨平台的线程创建、同步和通信功能。下面是一些关键知识点: 1. **线程创建**:pthread库的`pthread_create()`函数用于创建新线程。它需要传入...

    linuxduoxiancheng.rar_Linux下 线程_linux 多线程_linux多线程

    Linux下的多线程编程是操作系统...总的来说,理解并熟练掌握Linux下的多线程编程是提升软件开发效率的关键技能之一,这不仅涉及基本的创建和管理,还包括了如何有效地同步和控制线程,以实现高效、安全的并发程序设计。

    Pthread-Primer.rar_pthread_unix primer

    《Pthread Primer》是一本专为Unix环境下的多线程编程设计的指南,它深入浅出地介绍了pthread库,这是Unix系统中广泛使用的线程API。Pthread是POSIX线程(Portable Operating System Interface for Unix)的简称,是...

    Pthread多线程编程指南

    《Pthread多线程编程指南》是一本专为开发者准备的深入解析Pthread线程库的手册,尤其适合那些在UNIX、Linux等操作系统环境中进行多线程编程的工程师。Pthread,全称POSIX线程(Portable Operating System Interface...

    操作系统上机-Linux多线程编程

    在操作系统的学习过程中,掌握Linux多线程编程是至关重要的技能之一。Linux作为一款广泛使用的开源操作系统,其强大的系统调用接口和丰富的库函数为开发者提供了实现多线程应用的便利条件。多线程编程允许程序同时...

    linux多线程手册

    ### Linux多线程手册知识点详解 #### 一、多线程基础介绍 ##### 定义多线程术语 - **线程**:是进程中的一个执行单元,它共享进程的资源,但拥有独立的栈和寄存器上下文。 - **多线程**:指在一个进程中同时运行...

    linux多线程编程.pdf

    Linux 多线程编程是指在 Linux 操作系统中使用多线程技术来提高程序的执行效率和响应速度。多线程编程可以让程序同时执行多个任务,从而提高程序的整体性能。 线程基础知识 什么是线程?线程(Thread)是操作系统...

    linux C 多线程编程典型实例

    多线程间的同步至关重要,以防止数据竞争和死锁等问题。可以使用`pthread_mutex_t`类型的互斥锁实现资源的互斥访问。`pthread_mutex_lock()`和`pthread_mutex_unlock()`用于锁定和解锁互斥锁。`pthread_cond_t`条件...

    多线程编程材料

    Linux环境下多线程编程通常使用POSIX线程(pthread)库,它提供了创建、管理线程的API。 在Linux多线程编程中,每个线程都有自己的标识符pthread_t,这与传统的进程标识符pid_t不同。pthread_t通常并不是一个整数...

    Linux多线程程序设计.ppt

    在Linux中,POSIX线程库(pthread)是实现多线程编程的主要接口,它提供了头文件`&lt;pthread.h&gt;`,包含了创建、管理线程以及进行线程同步的各种函数。以下是一些关键的线程操作: 1. **线程ID和创建**: - `pthread_...

    关于Linux多线程编程.pdf

    Linux多线程编程是现代操作系统提供的强大功能之一,它允许开发者将程序分割成多个可同时执行的线程,提高程序执行效率和响应速度。多线程编程主要涉及到线程的创建、控制、同步以及线程间的数据共享等问题。以下将...

    linux的多线程编程的高效开发经验.pdf

    Linux上的线程API主要包括`pthread_create`用于创建线程,`pthread_exit`用于线程退出,`pthread_join`用于等待线程结束,以及`pthread_cancel`用于取消线程。互斥锁由`pthread_mutex_init`和`pthread_mutex_...

    关于Linux多线程编程

    Linux下的多线程编程主要涉及的是pthread库,它是POSIX线程库的一种实现,用于在Linux操作系统上创建和管理线程。线程是程序中的执行流,它与进程不同,进程是资源分配的基本单位,而线程是执行调度的基本单位。在...

    linux 线程 基础例程

    通过这个基础例程,学习者可以了解Linux线程的基本操作,并逐步深入到多线程编程的复杂场景,如死锁预防、资源管理等。实际应用中,理解线程同步和通信的重要性,能够有效地避免竞态条件,提高程序的稳定性和效率。

    linux多线程编程

    总结,Linux下的多线程编程涉及线程的创建、同步、退出等多个方面,通过熟练掌握pthread库的使用,可以构建高效稳定的多线程应用程序。在实际开发中,理解并合理运用这些知识点对于提升软件性能至关重要。通过阅读...

    linux多线程编程测试例子

    这个"linux多线程编程测试例子"可能是为了演示如何在实际项目中应用上述概念和技巧,通过编写和运行`pthread_test`程序,我们可以更深入地理解和掌握Linux下的多线程编程。在分析和调试这个例子时,需要关注线程间的...

    学习linux多线程编程的好资料

    Linux下的多线程编程主要依赖于pthread库,它提供了创建、管理线程的一系列函数。 - **创建缺省线程**:使用`pthread_create()`函数创建一个新的线程,这个新线程将执行指定的函数。 - **等待线程终止**:通过`...

    LINUX多线程

    在Linux操作系统中,多线程是一种程序执行方式,它允许单个进程内同时运行多个并发执行的线程。多线程技术为程序设计提供了更高级别的并发性,使得程序能够更高效地利用系统资源,特别是在处理器核心数量不断增加的...

    超多的linux线程编程的书籍

    1. 线程安全:确保函数在多线程环境下也能正确工作,避免数据竞争和死锁。 2. 资源管理:合理分配和释放线程资源,避免内存泄漏。 3. 线程同步:使用互斥锁、信号量、条件变量等机制确保线程间正确同步,防止数据不...

Global site tag (gtag.js) - Google Analytics