您的位置:首页 > 其它

pthread_cancel引起的死锁

2013-11-19 08:07 417 查看
说明:本文由【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:34if (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:38if (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:13pthread_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:44pthread_cancel(tid[0]);(gdb) nin thread 1 tag 1
Breakpoint 11, main () at testthread.cpp:46pthread_join(tid[0], NULL);(gdb) n[Switching to Thread 0xb77ebb70 (LWP 2495)]
Breakpoint 2, thread1 (arg=0x0) at testthread.cpp:24pthread_mutex_lock(&mutex);(gdb) n[Thread 0xb7fecb70 (LWP 2494) exited][Switching to Thread 0xb7fee6d0 (LWP 2491)]
Breakpoint 12, main () at testthread.cpp:47pthread_join(tid[1], NULL);(gdb) n^CProgram received signal SIGINT, Interrupt.0x00110416 in __kernel_vsyscall ()(gdb) info breakNum     Type           Disp Enb Address    Whatbreakpoint     keep y   0x08048742 in thread0(void*)at testthread.cpp:13breakpoint already hit 1 timebreakpoint     keep y   0x080487a4 in thread1(void*)at testthread.cpp:24breakpoint already hit 1 timebreakpoint     keep y   0x08048762 in thread0(void*)at testthread.cpp:15breakpoint     keep y   0x0804877a in thread0(void*)at testthread.cpp:17breakpoint     keep y   0x080487bc in thread1(void*)at testthread.cpp:26breakpoint     keep y   0x080487c8 in thread1(void*)at testthread.cpp:27breakpoint     keep y   0x080487e0 in thread1(void*)at testthread.cpp:29breakpoint     keep y   0x080487f5 in main() at testthread.cpp:34breakpoint already hit 1 timebreakpoint     keep y   0x0804882e in main() at testthread.cpp:38breakpoint already hit 1 timebreakpoint     keep y   0x08048882 in main() at testthread.cpp:44breakpoint already hit 1 time---Type <return> to continue, or q <return> to quit---breakpoint     keep y   0x0804888e in main() at testthread.cpp:46breakpoint already hit 1 timebreakpoint     keep y   0x080488a2 in main() at testthread.cpp:47breakpoint already hit 1 timebreakpoint     keep y   0x080488b6 in main() at testthread.cpp:49(gdb)
我们发现,Breakpoint 12, main () at testthread.cpp:4747 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):126 /* Before we block we enable cancellation.Therefore we have to127 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 asynchronouscancellation86 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 handlerpthread_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 Manpagehttp://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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: