本文简单介绍了一些 虚假唤醒(spurious wakeup) 相关的知识
(注: 本文假设读者对多线程开发有一定了解)
高层次的多线程编程中,条件变量是个常见的同步方法,跟传统仅使用互斥量的方法相比,条件变量可以减少锁的竞争.
拿 Pthread 举例,一个常见的条件变量的使用示例大概是这个样子的:
// flag for sync
bool g_signaled = false;
pthread_mutex_t g_mutex;
pthread_cond_t g_cond;
// wait method
void wait()
{
pthread_mutex_lock(&g_mutex);
while (!g_signaled)
{
pthread_cond_wait(&g_cond, &g_mutex);
}
g_signaled = false;
pthread_mutex_unlock(&g_mutex);
}
// signal method
void signal()
{
pthread_mutex_lock(&g_mutex);
g_signaled = true;
pthread_mutex_unlock(&g_mutex);
pthread_cond_signal(&g_cond);
}
代码中调用的 pthread_cond_wait 方法,作用在于可以让线程释放对应的互斥锁(g_mutex)并进入等待状态,然后在对应的条件变量(g_cond) signal 之后重新被唤醒并再次获取互斥锁.
上述示例代码中,我们在设置 g_signaled 之后调用了 pthread_cond_signal,正常来讲的话,之前调用 pthread_cond_wait 的线程会被唤醒,此时 g_signaled 应该一定为真,但是细心的朋友应该会发现,代码中我们却使用了一个循环来检查 g_signaled 的真值(并在发现 g_signaled 不为真时释放互斥锁然后重新进入了等待(通过重新调用 pthread_cond_wait)):
while (!g_signaled)
{
pthread_cond_wait(&g_cond, &g_mutex);
}
这么做的一个原因便是为了处理 虚假唤醒(spurious wakeup),所谓 虚假唤醒,指的是即便我们没有 signal 相关的条件变量(即没有调用 pthread_cond_signal),等待(调用了 pthread_cond_wait)的线程也可能被(虚假)唤醒,此时我们必须重新检查对应的标记值(以确认是否发生了(虚假)唤醒),又由于(虚假)唤醒可能会发生多次,所以我们最终需要使用循环来进行标记值检查.
虚假唤醒看上去很恼人,似乎我们应该在接口层消除这种现象(即让 pthread_cond_wait 不产生虚假唤醒),但有两个原因让虚假唤醒最终保留了下来:
1. 消除虚假唤醒非常困难
这里我们不对此做细节讲述,这里有些相关的讲解,有兴趣展开的朋友可以首先看看.
2. 即使消除了虚假唤醒,我们仍然需要循环检查标记值
这可能令人比较意外,问题在于除了虚假唤醒,还有一种称为 stolen wakeups 的现象也可能会影响标记值.
考虑下面的代码:
pthread_mutex_unlock(&g_mutex);
// gap here ...
pthread_cond_signal(&g_cond);
可以看到我们首先释放了互斥锁,接着 signal 了对应的条件变量,但是这两个操作之间是有"空隙"的,某一线程完全可以在这之间获取到互斥锁,改变标记值,然后再释放互斥锁,这导致标记值在 pthread_mutex_unlock 和 pthread_cond_signal 之间可能会发生变化,基于此,我们便仍然需要循环检查标记值(以防执行错误的逻辑).