条件变量的伪唤醒

起因是这样的,C++的新标准库添加了多线程的支持,有线程和线程同步的实现。我自己是做Windows开发比较多,自然用过Event这样的线程同步对象。但是这个东西,在C++的标准库里面没有。我想到说它有一个“条件变量”,而这个“条件变量”是有一个wait / signal 功能的,这个类看起来能实现 Event 的一部分功能。但是在前几天,有个同事说条件变量的 wait 会在没有被 signal 的时候也自己唤醒。毕竟平时不做 posix 那样系统下面的开发,所以这样的事情没有听说过。那会儿其实我是不信的,但是用关键字“condition variable fake wake”在网上搜了一下以后,看到确实是有这样的事情,然后英语中也不是叫 fake,管它叫 spurious wakeup。

在网上搜了一大圈,结合多个文章和问答以后,得知在没有其他线程调用 pthread_cond_signal 或者 pthread_cond_broadcast 这样的调用的情况下, pthread_cond_wait 也可能会提前返回。程序员在调用 pthread_cond_wait 的时候,要准备好它可能在没有收到信号的时候就已经醒来的情况。通常的做法是把 pthread_cond_wait 放在一个 while 循环里,循环的条件是条件变量要等待的条件。这样在伪唤醒发生的时候,程序会再次检查条件是否满足。如果条件没有满足,执行流程会转到继续调用 pthread_cond_wait 那边去;如果满足,才继续执行。

在Stackoverflow上我找到了这样的解释: Linux 下的mutex,使用了一种叫做 futex 的更底层的对象来实现。这个 futex 有一个问题,就是在进程收到 signal (不是 conditional variable 的那个 signal,是 *nix 系统下用 kill 命令发送的那种“信号”)的时候,正在等待futex 而被挂起的线程,会被唤醒、并执行 signal 的处理函数。现在已经知道了什么一种能直观地看伪唤醒发生的情况,那么做一个实验吧。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

pthread_t mainThread;

void sigHandler(int arg)
{
    const char* p;
    if (pthread_equal(pthread_self(), mainThread))
        p = "the same";
    else
        p = "different";
    printf("SIGUSR1 signal received on %s thread\n", p);
}

int main()
{
    pthread_mutex_t pmutex;
    pthread_cond_t pcond;

    signal(SIGUSR1, sigHandler);

    printf("process id: %d\n", getpid());

    mainThread = pthread_self();

    pthread_mutex_init(&pmutex, 0);
    pthread_cond_init(&pcond, 0);
    pthread_mutex_lock(&pmutex);
    pthread_cond_wait(&pcond, &pmutex);
    puts("wake up!");
    pthread_mutex_unlock(&pmutex);
    return 0;
}

很显然,这代码里面因为只有一个线程在跑,所以永远没人会去唤醒 pthread_cond_wait 的等待。但是根据上面那些网上调查所得来的信息,所期望的结果是在有signal那样的信号发来的时候, 线程会被唤醒,于是就在程序里注册了一个signal处理函数,在信号来的时候会输出信息并判断执行信号处理函数的线程是不是主线程,然后输出是不是在主线程收到信号的信息,然后pthread_cond_wait返回,程序输出 wake up! 信息后退出。

因为程序用到了 pthread 所以编译的时候要带上 -lpthread 参数。我在虚拟机里面编译运行这个程序,然后用 kill -SIGUSR1 给它发信号,结果看到了这样的输出。

screenshot1

与所期望的结果一致。

那么现在问题是,这个 pthread_cond_wait 它为什么被唤醒然后发现不是 pthread_cond_signal 做的后,是返回而不是继续回去等待呢?

在 pthread_cond_signal 函数的说明里,有这么样的描述

The pthread_cond_broadcast() and pthread_cond_signal() functions shall have no effect if there are no threads currently blocked on cond.

也就是说,如果当前没有线程正在等待,那么就没效果。这个和Windows下的手动重置事件是不一样的。

结合前面说的,信号来的时候线程会被唤醒,那么这个线程此时就不在 blocked 状态。因为在 pthread_cond_wait 等待的时候,和条件变量一起使用的锁是被释放的,所以其他线程在这段时间里是可以拿到锁、修改条件变量的等待条件、并且调用 pthread_cond_signal 的;但此时调用 pthread_cond_wait 的线程因为不是 blocked 状态,所以 pthread_cond_signal 的调用其实没有产生效果。如果 pthread_cond_wait 函数不再次检查条件、而是直接回去等待,那么就错过了这次机会,还有可能因此造成死锁(因为相当于没通知到)。

线程1:---mutex上锁---------------检查条件不满足-------等待-----------------------------

-----被kill发的信号唤醒------------------(此时应该做什么?)--------

线程2:-------------尝试锁mutex----------------------------锁mutex成功----改变了检查条件-

---------------------pthread_cond_signal---解锁mutex------------

以上示例中,线程1因为 kill 的信号而错过了 pthread_cond_signal 的信号,如果继续回去等待。那么可能就再也没机会醒过来了。

所以条件变量的使用方式是这么设计的:先加锁,然后一个while循环,如果条件不满足那么就在条件变量上等待,等待结束之后再次检查条件是不是满足:因为如果伪唤醒发生了,那么这个时候条件其实是不满足的。

参考:

http://stackoverflow.com/questions/21411912/what-can-wake-up-a-conditional-variable
http://man7.org/linux/man-pages/man2/futex.2.html
https://linux.die.net/man/3/pthread_cond_signal

《条件变量的伪唤醒》上有1条评论

发表评论