这篇文章主要从一个 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
#include #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; } |
示例代码_对上述程序的跟踪
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
|
[Thread debugging using libthread_db enabled] Breakpoint 8, main () at testthread.cpp: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 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 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 pthread_cancel(tid[0]); (gdb) n in thread 1 tag 1 Breakpoint 11, main () at testthread.cpp:46 pthread_join(tid[0], NULL); (gdb) n [Switching to Thread 0xb77ebb70 (LWP 2495)] Breakpoint 2, thread1 (arg=0x0) at testthread.cpp: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 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 breakpoint keep y 0x08048742 in thread0(void*) at testthread.cpp:13 breakpoint already hit 1 time breakpoint keep y 0x080487a4 in thread1(void*) at testthread.cpp:24 breakpoint already hit 1 time breakpoint keep y 0x08048762 in thread0(void*) at testthread.cpp:15 breakpoint keep y 0x0804877a in thread0(void*) at testthread.cpp:17 breakpoint keep y 0x080487bc in thread1(void*) at testthread.cpp:26 breakpoint keep y 0x080487c8 in thread1(void*) at testthread.cpp:27 breakpoint keep y 0x080487e0 in thread1(void*) at testthread.cpp:29 breakpoint keep y 0x080487f5 in main() at testthread.cpp:34 breakpoint already hit 1 time breakpoint keep y 0x0804882e in main() at testthread.cpp:38 breakpoint already hit 1 time breakpoint keep y 0x08048882 in main() at testthread.cpp:44 breakpoint already hit 1 time ---Type to continue, or q to quit--- breakpoint keep y 0x0804888e in main() at testthread.cpp:46 breakpoint already hit 1 time breakpoint keep y 0x080488a2 in main() at testthread.cpp:47 breakpoint already hit 1 time 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 中:
示例代码
|
/* Enable asynchronous cancellation. Required by the standard. */ cbuffer.oldtype = __pthread_enable_asynccancel (); 148 /* Wait until woken by signal or broadcast. */ lll_futex_wait (&cond->__data.__futex, futex_val); 151 /* Disable asynchronous cancellation. */ __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):
|
/* Before we block we enable cancellation. Therefore we have to install a cancellation handler. */ __pthread_cleanup_push (&buffer, __condvar_cleanup, &cbuffer); |
那么这个线程 清理程序 __condvar_cleanup 干了什么事情呢?我们可以注意到在它的实现最后(glibc-2.6/nptl/pthread_cond_wait.c):
|
/* Get the mutex before returning unless asynchronous cancellation is in effect. */ __pthread_mutex_cond_lock (cbuffer->mutex); } |
哦,__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
[4]http://blog.csdn.net/yanook/article/details/6589798
文章来自:Just Steps
《POSIX多线程程序设计》5.3.1中本身有提到过,对于延迟取消的状态下,“如果没有取消是当前未解决的,函数(取消点)将继续执行。如果当线程正在等一些东西(例如I/O)时,另外的线程请求取消该线程,那么等待将被打断并且线程将开始它的取消清除”
看glibc的实现确实更清晰,原来内部开启了异步取消,并且写了cleanUp函数!