日期:2014-05-16  浏览次数:21013 次

[百度分享]linux线程同步浅析 - 睡眠与唤醒的秘密
作者: 百度工程师 XY

一个程序问题 
之前写过这样一个C程序:模块维护一个工作线程、提供一组调用接口(分同步调用和异步调用)。用户调用模块提供的接口后,会向工作队列添加一个任务。然后任务由工作线程来处理。在同步调用情况下,接口调用后调用者被阻塞,等待工作线程处理完成后,将调用者唤醒。伪代码如下: 
[调用接口] 
add_command(cmd, pid); /* 1 */
raise(SIGSTOP); /* 2 */
get_response(cmd); /* 6 */
[工作线程] 
wait_for_command(&cmd, &pid); /* 3 */
do_command(cmd); /* 4 */
kill(pid, SIGCONT); /* 5 */
调用接口向工作队列添加命令以后,向自己发送一个SIGSTOP信号,把自己挂起;工作线程处理命令完成,通过向调用者进程发送SIGCONT信号,将调用者唤醒。 
流程上还是比较清晰的,但是有点想当然了。测试发现,程序的执行流程可能变成下面的情况: 
[调用接口] 
add_command(cmd, pid); /* 1 */
raise(SIGSTOP); /* 5 ... */
get_response(cmd);  
[工作线程] 
wait_for_command(&cmd, &pid); /* 2 */
do_command(cmd); /* 3 */
kill(pid, SIGCONT); /* 4 */
调用者在添加命令后,发生调度,工作线程在调用者进入睡眠之前,先处理了命令并发出唤醒信号。之后,调用者再睡眠,就没办法被唤醒了。 
解决方法 
直接使用信号来实现睡眠和唤醒看来是不可取的,于是想到了使用pthread的互斥机制。改写后的程序如下: 
[调用接口] 
add_command(cmd); /* 1 */
pthread_cond_wait(cond); /* 2 */
get_response(cmd); /* 6 */
[工作线程] 
wait_for_command(&cmd, &pid); /* 3 */
do_command(cmd); /* 4 */
pthread_cond_signal(cond); /* 5 */
测试发现,这样做就不会出现由于调度而出现“先唤醒、后睡眠”的问题了。 
但是,pthread条件变量是如何避免“先唤醒、后睡眠”的呢?实际上,它依然无法避免调用者在添加命令后,由于调度,造成pthread_cond_signal先于pthread_cond_wait发生的问题。但是条件变量内部记录了信号是否已发生,如果pthread_cond_signal先于pthread_cond_wait,则pthread_cond_wait将看到条件变量中记录的“信号已发生”,于是放弃睡眠。 
man一下pthread_cond_signal可以看到如下流程: 
[pthread_cond_wait(mutex, cond)] 
value = cond->value; /* 1 */
pthread_mutex_unlock(mutex); /* 2 */
pthread_mutex_lock(cond->mutex); /* 10 */
if (value == cond->value) { /* 11 */
  me->next_cond = cond->waiter;
  cond->waiter = me;
  pthread_mutex_unlock(cond->mutex); /* X */
  unable_to_run(me); /* Y */
} else
pthread_mutex_unlock(cond->mutex); /* 12 */
pthread_mutex_lock(mutex); /* 13 */
[pthread_cond_signal(cond)] 
pthread_mutex_lock(cond->mutex); /* 3 */
cond->value++; /* 4 */
if (cond->waiter) { /* 5 */
  sleeper = cond->waiter; /* 6 */
  cond->waiter = sleeper->next_cond; /* 7 */
  able_to_run(sleeper); /* 8 */
}
pthread_mutex_unlock(cond->mutex); /* 9 */
这份伪代码中的cond->value就是用于记录“信号已发生”的变量。 
深入一点 
如果你足够细心,可能已经发现上面的pthread的伪代码是有问题的。在‘X’处,cond->value已经判断过了,cond->mutex也已经释放了,而unable_to_run(将进程挂起)还没运行机制。那么此时如果发生调度,pthread_cond_signal先运行了呢?是不是able_to_run(唤醒)又将发生在unable_to_run之前,而导致“先唤醒、后睡眠”呢? 
这就变成了下面的流程: 
[pthread_cond_wait(mutex, cond)] 
value = cond->value; /* 1 */
pthread_mutex_unlock(mutex); /* 2 */
pthread_mutex_lock(cond->mutex); /* 3 */
if (value == cond->value) { /* 4 */
  me->next_cond = cond->waiter;
  cond->waiter = me;
  pthread_mutex_unlock(cond->mutex); /* 5 */
  unable_to_run(me); /* 13 ... */
} else
pthread_mutex_unlock(cond->mutex);
pthread_mutex_lock(mutex);
[pthread_cond_signal(cond)] 
pthread_mutex_lock(cond->mutex); /* 6 (注意:5已经释放锁了) */
cond->value++; /* 7 */
if (cond->waiter) { /* 8 */