现在的位置: 首页 > 综合 > 正文

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

2013年08月30日 ⁄ 综合 ⁄ 共 7516字 ⁄ 字号 评论关闭

说明:本文由【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

抱歉!评论已关闭.