背景:

进程中包括下面三个线程:

  • 主线程main_thread,通过用户输入来执行相应的操作,包括:启动写数据线程,杀死写数据线程等;
  • 定时器线程timer_thread,每两分钟写一次数据;
  • 写数据线程write_thread,不停写数据。因为定时器线程和写数据线程会操作同一个全局变量,所以使用线程互斥锁进行了加锁操作。

三个线程的简单流程如下:

/* main thread */
int main()
{
  ......
switch (user_ops){
  case 'w':
    pthread_create(&write_thread,NULL....);
    break;
  case 'k':
    pthread_cancel(&write_thread);
    break;
 }
  ......
}
/* write_thread */
{
  ......
  pthread_mutex_lock(&entry_lock);
  write_data();
  update_entry();
  pthread_mutex_unlock(&entry_lock);
  ......
}
/* timer_thread */
{
  ......
  pthread_mutex_lock(&entry_lock);
  write_entry_to_disk();
  pthread_mutex_unlock(&entry_lock);
  ......
}

死锁现象

  1. 模拟用户操作,向main_thread中传入w命令,创建写数据线程;
  2. 写入一定量的数据之后,传入k命令,取消写数据线程;
  3. 在定时器到时,有一定的几率导致忙等。

原因分析

初步分析: 写数据线程对磁盘加锁后,还没有解锁便被取消掉了,同步线程在获取锁的地方忙等。

深入分析: man pthread_cancel 可以知道,调用 pthread_cancel 之后,具体会发生什么事情,是由两个状态决定的:cancelstate 和 canceltype。

  • cancelstate 有 enabled 和 disabled 两个取值,当其值是 enable 的时候根据 canceltype 的值进行后续操作,否则将 cancel 请求加入到请求队列,等待 cancelstate 变为 enabled 的时候再进行后续操作。 默认值是 enabled。
  • canceltype 也有两个取值: deferred 和 asynchronous,分别是延迟取消和异步取消。 
    延迟取消是指线程收到取消请求,并不作出任何响应,而是要到取消点(cancellation point)的时候才执行取消动作,取消动作包括:
    1. 将 pthread_cleanup_push 压栈的函数出栈执行;
    2. 调用线程的析构函数;
    3. 调用 pthread_exit。
      异步取消可以在任何时候执行取消动作。 线程的默认取消类型是延迟取消

回到问题本身,write 系统调用刚好就是一个取消点(更多的取消点见《Unix环境高级编程》12.7章节),写数据进程有一定的概率在执行 write 系统调用的时候被取消掉,此时写数据进程还是持有锁的状态,从而造成定时器线程获取不到线程互斥锁而进行忙等。

解决方案

从线程被取消时执行的取消动作入手,在对线程互斥锁加锁前调用 pthread_cleanup_push ,将解锁操作压栈作为线程清理函数,在解锁后调用 pthread_cleanup_pop。 如下所示:

pthread_cleanup_push(pthread_mutex_unlock, (void *) &mutex);  

pthread_mutex_lock(&mutex);
/* do some work */
pthread_mutex_unlock(&mutex);                                                        

pthread_cleanup_pop(0);                        

pthread_cleanup_pop 传入的参数为0,表示不执行出栈操作。如果线程被 pthread_cancel 取消时,会将压栈的清理函数全部出栈执行,这样就会对互斥量解锁, 如果正常退出则不执行。

扩展阅读

延伸到 linux 的另一种同步机制: 条件变量。pthread_cancel 遇到条件变量时隐藏了更加复杂的竞争问题, 同时了解 pthread_cond_wait 如何实现取消点。

条件变量需要配合互斥锁一起使用,其目的是为了“允许线程以无竞争的方式等待特定的条件发生”。 使用条件变量的情景如下:

pthread1:
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
/* do something */
pthread_mutex_unlock(&mutex);

pthread2:
pthread_mutex_lock(&mutex);
/* do something to change the condition */
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);

线程 1 对全局互斥量加锁,然后调用 pthread_cond_wait 函数等待条件发生,该函数也恰好是一个取消点。 pthread_cond_wait 以原子操作释放互斥量并且将线程放到等待条件的线程列表上,以保证解锁后不错过任何符合的条件, 然后进入睡眠,直到条件发生将其唤醒;

线程 2 获取全局互斥锁,进行一些操作导致条件发生,并调用 pthread_cond_broadcast 广播该条件,然后释放锁资源。

线程 1 被唤醒,重新获取互斥量(这个操作在pthread_cond_wait中发生),然后进行一些处理后释放锁资源。

(这种处理有点类似 DMA,不去询问是否有数据,而是数据来了通知其处理。)

重点是 pthread_cond_wait 函数,查看 glibc 源码:

..........
err = __pthread_mutex_unlock_usercnt (mutex, 0);                            /* 释放互斥锁 */
.........
pthread_cleanup_push (&buffer, condvar_cleanup, &cbuffer);     /* 压栈了一个线程清理函数 */
..........
do{
.......
/* Enable asynchronous cancellation.  Required by the standard.  */
 cbuffer.oldtype = __pthread_enable_asynccancel ();        /* 将线程的取消类型设置为异步取消 */
  /* Wait until woken by signal or broadcast.  */
  lll_futex_wait (&cond->__data.__futex, futex_val, pshared);          /* 进入睡眠等待条件发生 */

  /* Disable asynchronous cancellation.  */                                 /* 将线程的取消类型修改回延迟取消 */
  __pthread_disable_asynccancel (cbuffer.oldtype);
  ..........
}while......

压栈的线程清理函数 pthread_cleanup_push 做了很多东西我们都不管,最重要的是,最后一句: __pthread_mutex_cond_lock (cbuffer->mutex); 获取互斥锁。

获取互斥锁,是为了使得 pthread_cond_wait 调用前后的状态一致,同时还有 POSIX 一部分原因。

现在可以看出端倪了,如果在进入睡眠等待条件发生的时候, 该线程被其他线程 cancel 掉了,这是完全可能的,因为睡眠前设置了异步取消,那么 cancel 时会执行线程清理函数,重新对互斥量进行加锁。也就是说, 执行完 pthread_cancel 之后, 互斥锁是被持有的,并且持有它的线程已经退出了,导致其他要获取该互斥锁的线程进入忙等。

其实本质上和互斥量的情况是一样的,只是条件变量的情况稍微复杂了一点。解决办法也是一样的,在调用 pthread_mutex_lock 之前 调用 pthread_cleanup_push(pthread_mutex_unlock, &mutex), 调用 pthread_mutex_unlock 之后调用 pthread_cleanup_pull(0)。按照压栈的顺序,出栈时,先调用 pthread_cond_wait 中压入的 __condvar_cleanup 函数进行加锁,然后调用我们在 pthread_mutex_lock 前压入的 pthread_mutex_unlock 函数进行解锁, 这样 pthread_cancel 执行完之后,互斥量是没有被持有的。

上述内容如有整理不当或者错漏之处烦请指点。

参考资料:

《Unix环境高级编程》,linux manual page,glibc源码

 posted on 2017-06-11 20:35  蠢动的染色体  阅读(602)  评论(0编辑  收藏  举报