std::condition_variable 内存写入可见性
std::condition_variable memory writes visibility
std::condition_variable::notify_one()
或 std::condition_variable::notify_all()
是否保证在调用之前在当前线程中写入的非原子内存在通知的线程中可见?
其他线程做:
{
std::unique_lock lock(mutex);
cv.wait(lock, []() { return values[threadIndex] != 0; });
// May a thread here see a zero value and therefore start to wait again?
}
主线程做:
fillData(values); // All values are zero and all threads wait() before calling this.
cv.notify_all(); // Do need some memory fence or lock before this
// to ensure that new non-zero values will be visible
// in other threads immediately after waking up?
notify_all() 不会存储一些原子值从而强制执行内存排序吗?我没说清楚
UPD:根据 Superlokkus 的回答和 an answer here:我们必须获取锁以确保内存写入在其他线程中的可见性(内存传播),否则线程在我的例子中可能会读取零值。
我还错过了这句话 here about condition_variable,它专门回答了我的问题。在修改必须立即可见的情况下,即使是原子变量也必须在锁下修改。
Even if the shared variable is atomic, it must be modified under the
mutex in order to correctly publish the modification to the waiting
thread.
我猜你混淆了所谓的原子值的内存排序和基于经典锁的同步机制。
当你有一个在线程之间共享的数据时,假设一个 int
例如,一个线程不能简单地读取它而另一个线程可能同时写入它。否则我们会发生数据竞争。
为了长期解决这个问题,我们使用了经典的基于锁的同步:
这些线程至少共享一个互斥量和 int
。要读取或写入任何线程必须首先持有锁,这意味着它们等待互斥量。构建互斥体是为了让它们可以同时发生。如果一个线程赢得互斥锁,它可以更改或读取 int
然后解锁它,这样其他线程也可以 read/write。使用像您使用的条件变量只是为了使“读者等待作者更改值”的模式更有效,他们被 cv 唤醒,而不是周期性地等待锁定、读取和解锁,这会被称为忙等
因此,因为您在等待互斥锁之后持有锁,或者在您的情况下,正确地(仍然需要互斥锁)等待条件变量,您可以更改 int
。在作者能够写出新值之后,读者将阅读新值,而不是旧值。 更新:但是如果必须添加一件事,这也可能是造成混淆的原因:条件变量受所谓的虚假唤醒的影响。这意味着即使您写入没有通知任何线程,读取线程可能仍会唤醒,并且互斥量已锁定。所以你必须检查你的作者是否真的把你叫醒了,这通常是由作者通过更改另一个数据来通知这一点,或者通过使用你已经想分享的相同数据来完成的。 std::condition_variable::wait
的 lambda 参数重载只是为了让检查和返回睡眠代码看起来更漂亮一些。根据你现在的问题,我不知道你是否想用你 values
来完成这份工作。
但是“主”线程的代码片段不正确或不完整:
您没有在互斥量上同步以更改 values
。
你必须为此持有锁,但通知可以在没有锁的情况下完成。
std::unique_lock lock(mutex);
fillData(values);
lock.unlock();
cv.notify_all();
但是这些基于互斥锁的模式有一些缺点并且速度很慢,一次只能有一个线程可以做某事。这就是所谓的原子,例如 std::atomic<int>
开始发挥作用。它们可以在没有互斥量的情况下由多个线程并发地同时写入和读取。内存排序只是在那里需要考虑的事情,并且是对以有意义的方式使用其中多个内存或者不需要“写入后,我永远不会看到旧值”保证的情况的优化。但是,使用它的默认内存排序 memory_order_seq_cst
你也可以。
std::condition_variable::notify_one()
或 std::condition_variable::notify_all()
是否保证在调用之前在当前线程中写入的非原子内存在通知的线程中可见?
其他线程做:
{
std::unique_lock lock(mutex);
cv.wait(lock, []() { return values[threadIndex] != 0; });
// May a thread here see a zero value and therefore start to wait again?
}
主线程做:
fillData(values); // All values are zero and all threads wait() before calling this.
cv.notify_all(); // Do need some memory fence or lock before this
// to ensure that new non-zero values will be visible
// in other threads immediately after waking up?
notify_all() 不会存储一些原子值从而强制执行内存排序吗?我没说清楚
UPD:根据 Superlokkus 的回答和 an answer here:我们必须获取锁以确保内存写入在其他线程中的可见性(内存传播),否则线程在我的例子中可能会读取零值。
我还错过了这句话 here about condition_variable,它专门回答了我的问题。在修改必须立即可见的情况下,即使是原子变量也必须在锁下修改。
Even if the shared variable is atomic, it must be modified under the mutex in order to correctly publish the modification to the waiting thread.
我猜你混淆了所谓的原子值的内存排序和基于经典锁的同步机制。
当你有一个在线程之间共享的数据时,假设一个 int
例如,一个线程不能简单地读取它而另一个线程可能同时写入它。否则我们会发生数据竞争。
为了长期解决这个问题,我们使用了经典的基于锁的同步:
这些线程至少共享一个互斥量和 int
。要读取或写入任何线程必须首先持有锁,这意味着它们等待互斥量。构建互斥体是为了让它们可以同时发生。如果一个线程赢得互斥锁,它可以更改或读取 int
然后解锁它,这样其他线程也可以 read/write。使用像您使用的条件变量只是为了使“读者等待作者更改值”的模式更有效,他们被 cv 唤醒,而不是周期性地等待锁定、读取和解锁,这会被称为忙等
因此,因为您在等待互斥锁之后持有锁,或者在您的情况下,正确地(仍然需要互斥锁)等待条件变量,您可以更改 int
。在作者能够写出新值之后,读者将阅读新值,而不是旧值。 更新:但是如果必须添加一件事,这也可能是造成混淆的原因:条件变量受所谓的虚假唤醒的影响。这意味着即使您写入没有通知任何线程,读取线程可能仍会唤醒,并且互斥量已锁定。所以你必须检查你的作者是否真的把你叫醒了,这通常是由作者通过更改另一个数据来通知这一点,或者通过使用你已经想分享的相同数据来完成的。 std::condition_variable::wait
的 lambda 参数重载只是为了让检查和返回睡眠代码看起来更漂亮一些。根据你现在的问题,我不知道你是否想用你 values
来完成这份工作。
但是“主”线程的代码片段不正确或不完整:
您没有在互斥量上同步以更改 values
。
你必须为此持有锁,但通知可以在没有锁的情况下完成。
std::unique_lock lock(mutex);
fillData(values);
lock.unlock();
cv.notify_all();
但是这些基于互斥锁的模式有一些缺点并且速度很慢,一次只能有一个线程可以做某事。这就是所谓的原子,例如 std::atomic<int>
开始发挥作用。它们可以在没有互斥量的情况下由多个线程并发地同时写入和读取。内存排序只是在那里需要考虑的事情,并且是对以有意义的方式使用其中多个内存或者不需要“写入后,我永远不会看到旧值”保证的情况的优化。但是,使用它的默认内存排序 memory_order_seq_cst
你也可以。