C++ 使用std::atomic和std::condition_变量进行同步不可靠

C++ 使用std::atomic和std::condition_变量进行同步不可靠,c++,multithreading,c++11,stl,C++,Multithreading,C++11,Stl,在用C++11编写的分布式作业系统中,我使用以下结构实现了一个围栏(即工作线程池外的线程可能会要求阻塞,直到所有当前计划的作业完成): struct fence { std::atomic<size_t> counter; std::mutex resume_mutex; std::condition_variable resume

在用C++11编写的分布式作业系统中,我使用以下结构实现了一个围栏(即工作线程池外的线程可能会要求阻塞,直到所有当前计划的作业完成):

struct fence
{
    std::atomic<size_t>                     counter;
    std::mutex                              resume_mutex;
    std::condition_variable                 resume;

    fence(size_t num_threads)
        : counter(num_threads)
    {}
};
结构围栏
{
原子计数器;
std::mutex resume\u mutex;
std::条件变量恢复;
围栏(尺寸/数量/螺纹)
:计数器(线程数)
{}
};
实现围栏的代码如下所示:

void task_pool::fence_impl(void *arg)
{
    auto f = (fence *)arg;
    if (--f->counter == 0)      // (1)
        // we have zeroed this fence's counter, wake up everyone that waits
        f->resume.notify_all(); // (2)
    else
    {
        unique_lock<mutex> lock(f->resume_mutex);
        f->resume.wait(lock);   // (3)
    }
}
void task\u pool::fence\u impl(void*arg)
{
自动f=(围栏*)参数;
如果(--f->计数器==0)/(1)
//我们已经调零了栅栏的计数器,唤醒所有等待的人
f->resume.notify_all();/(2)
其他的
{
唯一锁定(f->resume\u mutex);
f->继续。等待(锁定);/(3)
}
}
如果线程在一段时间内进入围栏,这将非常有效。然而,如果他们几乎同时尝试这样做,似乎有时会发生这样的情况:在原子递减(1)和启动条件变量(3)上的等待之间,线程产生CPU时间,而另一个线程将计数器递减到零(1)并触发cond。var(2)。这导致前一个线程在(3)中永远等待,因为它在收到通知后开始等待

一个可行的方法是在(2)之前先睡10毫秒,但这显然是不可接受的


有没有关于如何以性能方式修复此问题的建议?

您的诊断是正确的,此代码很容易以您描述的方式丢失状态通知。即,在一个线程锁定互斥锁后,但在等待条件变量之前,另一个线程可能会调用notify_all(),以便第一个线程错过该通知

一个简单的修复方法是在递减计数器之前锁定互斥锁,同时通知:

void task_pool::fence_impl(void *arg)
{
    auto f = static_cast<fence*>(arg);
    std::unique_lock<std::mutex> lock(f->resume_mutex);
    if (--f->counter == 0) {
        f->resume.notify_all();
    }
    else do {
        f->resume.wait(lock);
    } while(f->counter);
}
void task\u pool::fence\u impl(void*arg)
{
自动f=静态_转换(arg);
std::unique_lock(f->resume_mutex);
如果(--f->计数器==0){
f->resume.notify_all();
}
否则会{
f->恢复。等待(锁定);
}while(f->counter);
}
在这种情况下,计数器不需要是原子的

在发出通知之前锁定互斥锁的额外好处(或惩罚,取决于观点)是(从):

线程可以调用pthread_cond_broadcast()或pthread_cond_signal()函数,无论其当前是否拥有调用pthread_cond_wait()或pthread_cond_timedwait()的线程在等待期间与条件变量关联的互斥锁;但是,如果需要可预测的调度行为,则该互斥锁应由调用pthread\u cond\u broadcast()或pthread\u cond\u signal()的线程锁定。

关于
while
循环(从):

可能会发生pthread_cond_timedwait()或pthread_cond_wait()函数的虚假唤醒。由于pthread_cond_timedwait()或pthread_cond_wait()的返回并不意味着与此谓词的值有关的任何内容,因此应在此类返回时重新计算谓词


为了保持原子操作的更高性能而不是完全互斥,应该将等待条件更改为锁定、检查和循环

所有条件等待都应以这种方式完成。条件变量甚至还有第二个要等待的参数,即谓词函数或lambda

代码可能如下所示:

void task_pool::fence_impl(void *arg)
{
    auto f = (fence *)arg;
    if (--f->counter == 0)      // (1)
        // we have zeroed this fence's counter, wake up everyone that waits
        f->resume.notify_all(); // (2)
    else
    {
        unique_lock<mutex> lock(f->resume_mutex);
        while(f->counter) {
            f->resume.wait(lock);   // (3)
        }
    }
}
void task\u pool::fence\u impl(void*arg)
{
自动f=(围栏*)参数;
如果(--f->计数器==0)/(1)
//我们已经调零了栅栏的计数器,唤醒所有等待的人
f->resume.notify_all();/(2)
其他的
{
唯一锁定(f->resume\u mutex);
while(f->counter){
f->继续。等待(锁定);/(3)
}
}
}

unique\u lock
缺少其所需的模板参数:
unique\u lock
,无论是在问题还是答案中。但除此之外,我同意,+1.哦,类中有一个
typedef
fence
是一个嵌套结构),但我已经添加了可读性的模板参数,谢谢。@Maxim Yegorushkin,为什么要添加
while(f->counter)我在C++的代码中添加了一个注释。不知道在Windows上如何实现条件变量,您可以在Boost条件变量源中查找它。您只添加了一个循环来捕捉虚假的唤醒。这并不能解决我最初描述的问题-一个线程(3)可能仍然会屈服,让另一个线程在原始线程最终到达(3)之前到达(2)。除此之外,“锁定、检查和循环”是指自旋锁吗?