我是否需要为线程之间的更改通知标志设置内存屏障?
Do I need a memory barrier for a change notification flag between threads?
我需要一个非常快速的(在 "low cost for reader" 的意义上,而不是 "low latency")线程之间的更改通知机制,以便更新读取缓存:
情况
线程 W
(编写器)仅偶尔更新数据结构 (S
)(在我的例子中是地图中的设置)。
线程 R
(Reader) 维护着一个 S
的缓存,并且非常频繁地读取它。当 Thread W
更新 S
Thread R
需要在合理的时间内(10-100ms)通知更新。
架构是 ARM、x86 和 x86_64。我需要用 gcc 4.6 及更高版本支持 C++03
。
代码
是这样的:
// variables shared between threads
bool updateAvailable;
SomeMutex dataMutex;
std::string myData;
// variables used only in Thread R
std::string myDataCache;
// Thread W
SomeMutex.Lock();
myData = "newData";
updateAvailable = true;
SomeMutex.Unlock();
// Thread R
if(updateAvailable)
{
SomeMutex.Lock();
myDataCache = myData;
updateAvailable = false;
SomeMutex.Unlock();
}
doSomethingWith(myDataCache);
我的问题
在线程 R
中 "fast path" 中没有发生锁定或障碍(没有可用的更新)。
这是一个错误吗?这种设计的后果是什么?
我需要将 updateAvailable
限定为 volatile
吗?
R
会 最终 获得更新吗?
我目前的理解
数据一致性安全吗?
这看起来有点像 "Double Checked Locking"。根据 http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html 可以使用内存屏障在 C++ 中修复它。
然而,这里的主要区别在于共享资源永远不会 touched/read 在 Reader 快速路径中。更新缓存时,一致性由mutex保证。
R
会得到更新吗?
这就是它变得棘手的地方。据我了解,CPU 运行 线程 R
可以无限期地缓存 updateAvailable
,有效地将读取方式移动到实际的 if
语句之前。
因此更新可能会持续到下一次缓存刷新,例如当另一个线程或进程被调度时。
使用 C++ 原子并使 updateAvailable
成为 std::atomic<bool>
。这样做的原因是,不仅仅是 CPU 可以看到旧版本的变量,尤其是编译器,它看不到另一个线程的副作用,因此从不费心重新获取变量,所以你永远不会查看线程中的更新值。此外,通过这种方式,您可以获得有保证的原子读取,而如果您只是读取值,则不会。
除此之外,您可能会摆脱锁,例如,如果生产者仅在 updateAvailable
为假时才生成数据,您可以摆脱互斥锁,因为 std::atomic<>
强制读取和写入的正确顺序。如果不是这种情况,您仍然需要锁。
Do I need to qualify updateAvailable
as volatile
?
由于 volatile 与 C++ 中的线程模型无关,您应该使用原子来使您的程序严格符合标准:
在 C++11
或更新的 上 方法是使用 atomic<bool>
和 memory_order_relaxed
store/load:
atomic<bool> updateAvailable;
//Writer
....
updateAvailable.store(true, std::memory_order_relaxed); //set (under mutex locked)
// Reader
if(updateAvailable.load(std::memory_order_relaxed)) // check
{
...
updateAvailable.store(false, std::memory_order_relaxed); // clear (under mutex locked)
....
}
gcc 自 4.7 起在其 atomic builtins.
中支持类似的功能
至于 gcc 4.6,在访问 updateAvailable
变量时似乎没有严格确认的方法来规避栅栏。实际上,内存栅栏通常比 10-100 毫秒的时间量级快得多。所以你可以使用它自己的 atomic builtins:
int updateAvailable = 0;
//Writer
...
__sync_fetch_and_or(&updateAvailable, 1); // set to non-zero
....
//Reader
if(__sync_fetch_and_and(&updateAvailable, 1)) // check, but never change
{
...
__sync_fetch_and_and(&updateAvailable, 0); // clear
...
}
Is it safe regarding data consistency?
是的,很安全。你的理由在这里完全正确:
the shared resource is never touched/read in the Reader fast path.
这不是双重检查锁定!
问题本身已经明确说明了。
如果 updateAvailable
为假,Reader 线程使用变量 myDataCache
,即 local 到线程(没有其他线程使用它)。使用双重检查锁定方案,所有线程直接使用 shared 对象。
为什么这里不需要内存 fences/barriers
并发访问的唯一变量是 updateAvailable
。 myData
变量是通过互斥保护访问的,它提供了所有需要的栅栏。 myDataCache
是 local 到 Reader 线程。
当 Reader 线程发现 updateAvailable
变量为 false 时,它使用 myDataCache
变量,该变量被更改为 由线程本身。 程序顺序保证在这种情况下更改的正确可见性。
至于变量updateAvailable
的可见性保证,C++11标准为原子变量提供了这样的保证,即使没有栅栏。 29.3 p13 说:
Implementations should make atomic stores visible to atomic loads within a reasonable amount of time.
Jonathan Wakely 已证实,本段甚至适用于 memory_order_relaxed
访问 in chat。
你必须在这里使用内存栅栏。如果没有围栏,则无法保证永远 会在另一个线程上看到更新。在 C++03 中,您可以选择使用特定于平台的 ASM 代码(Intel 上的 mfence
,不了解 ARM)或使用 OS 提供的原子 set/get 函数。
我需要一个非常快速的(在 "low cost for reader" 的意义上,而不是 "low latency")线程之间的更改通知机制,以便更新读取缓存:
情况
线程 W
(编写器)仅偶尔更新数据结构 (S
)(在我的例子中是地图中的设置)。
线程 R
(Reader) 维护着一个 S
的缓存,并且非常频繁地读取它。当 Thread W
更新 S
Thread R
需要在合理的时间内(10-100ms)通知更新。
架构是 ARM、x86 和 x86_64。我需要用 gcc 4.6 及更高版本支持 C++03
。
代码
是这样的:
// variables shared between threads
bool updateAvailable;
SomeMutex dataMutex;
std::string myData;
// variables used only in Thread R
std::string myDataCache;
// Thread W
SomeMutex.Lock();
myData = "newData";
updateAvailable = true;
SomeMutex.Unlock();
// Thread R
if(updateAvailable)
{
SomeMutex.Lock();
myDataCache = myData;
updateAvailable = false;
SomeMutex.Unlock();
}
doSomethingWith(myDataCache);
我的问题
在线程 R
中 "fast path" 中没有发生锁定或障碍(没有可用的更新)。
这是一个错误吗?这种设计的后果是什么?
我需要将 updateAvailable
限定为 volatile
吗?
R
会 最终 获得更新吗?
我目前的理解
数据一致性安全吗?
这看起来有点像 "Double Checked Locking"。根据 http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html 可以使用内存屏障在 C++ 中修复它。
然而,这里的主要区别在于共享资源永远不会 touched/read 在 Reader 快速路径中。更新缓存时,一致性由mutex保证。
R
会得到更新吗?
这就是它变得棘手的地方。据我了解,CPU 运行 线程 R
可以无限期地缓存 updateAvailable
,有效地将读取方式移动到实际的 if
语句之前。
因此更新可能会持续到下一次缓存刷新,例如当另一个线程或进程被调度时。
使用 C++ 原子并使 updateAvailable
成为 std::atomic<bool>
。这样做的原因是,不仅仅是 CPU 可以看到旧版本的变量,尤其是编译器,它看不到另一个线程的副作用,因此从不费心重新获取变量,所以你永远不会查看线程中的更新值。此外,通过这种方式,您可以获得有保证的原子读取,而如果您只是读取值,则不会。
除此之外,您可能会摆脱锁,例如,如果生产者仅在 updateAvailable
为假时才生成数据,您可以摆脱互斥锁,因为 std::atomic<>
强制读取和写入的正确顺序。如果不是这种情况,您仍然需要锁。
Do I need to qualify
updateAvailable
asvolatile
?
由于 volatile 与 C++ 中的线程模型无关,您应该使用原子来使您的程序严格符合标准:
在 C++11
或更新的 上 方法是使用 atomic<bool>
和 memory_order_relaxed
store/load:
atomic<bool> updateAvailable;
//Writer
....
updateAvailable.store(true, std::memory_order_relaxed); //set (under mutex locked)
// Reader
if(updateAvailable.load(std::memory_order_relaxed)) // check
{
...
updateAvailable.store(false, std::memory_order_relaxed); // clear (under mutex locked)
....
}
gcc 自 4.7 起在其 atomic builtins.
中支持类似的功能至于 gcc 4.6,在访问 updateAvailable
变量时似乎没有严格确认的方法来规避栅栏。实际上,内存栅栏通常比 10-100 毫秒的时间量级快得多。所以你可以使用它自己的 atomic builtins:
int updateAvailable = 0;
//Writer
...
__sync_fetch_and_or(&updateAvailable, 1); // set to non-zero
....
//Reader
if(__sync_fetch_and_and(&updateAvailable, 1)) // check, but never change
{
...
__sync_fetch_and_and(&updateAvailable, 0); // clear
...
}
Is it safe regarding data consistency?
是的,很安全。你的理由在这里完全正确:
the shared resource is never touched/read in the Reader fast path.
这不是双重检查锁定!
问题本身已经明确说明了。
如果 updateAvailable
为假,Reader 线程使用变量 myDataCache
,即 local 到线程(没有其他线程使用它)。使用双重检查锁定方案,所有线程直接使用 shared 对象。
为什么这里不需要内存 fences/barriers
并发访问的唯一变量是 updateAvailable
。 myData
变量是通过互斥保护访问的,它提供了所有需要的栅栏。 myDataCache
是 local 到 Reader 线程。
当 Reader 线程发现 updateAvailable
变量为 false 时,它使用 myDataCache
变量,该变量被更改为 由线程本身。 程序顺序保证在这种情况下更改的正确可见性。
至于变量updateAvailable
的可见性保证,C++11标准为原子变量提供了这样的保证,即使没有栅栏。 29.3 p13 说:
Implementations should make atomic stores visible to atomic loads within a reasonable amount of time.
Jonathan Wakely 已证实,本段甚至适用于 memory_order_relaxed
访问 in chat。
你必须在这里使用内存栅栏。如果没有围栏,则无法保证永远 会在另一个线程上看到更新。在 C++03 中,您可以选择使用特定于平台的 ASM 代码(Intel 上的 mfence
,不了解 ARM)或使用 OS 提供的原子 set/get 函数。