是否可以将宽松的内存顺序效果扩展到执行线程的生命之后?
Does relaxed memory order effect can be extended to after performing-thread's life?
假设在 C++11 程序中,我们有一个名为 A 的主线程,它启动一个名为 B 的异步线程。在线程 B 中,我们对具有 std::memory_order_relaxed
内存顺序的原子变量执行原子存储。然后线程 A 与线程 B 连接。然后线程 A 启动另一个名为 C 的线程,该线程以 std::memory_order_relaxed
内存顺序执行原子加载操作。难不成线程C加载的内容和线程B写的内容不一样?换句话说,这里宽松的内存一致性是否会延伸到线程生命周期之后?
为了尝试这个,我写了一个简单的程序,运行 尝试了很多次。该程序不会报告不匹配。我在想,因为线程 A 在线程的启动中强加了一个顺序,所以不匹配不会发生。但是,我不确定。
#include <atomic>
#include <iostream>
#include <future>
int main() {
static const int nTests = 100000;
std::atomic<int> myAtomic( 0 );
auto storeFunc = [&]( int inNum ){
myAtomic.store( inNum, std::memory_order_relaxed );
};
auto loadFunc = [&]() {
return myAtomic.load( std::memory_order_relaxed );
};
for( int ttt = 1; ttt <= nTests; ++ttt ) {
auto writingThread = std::async( std::launch::async, storeFunc, ttt );
writingThread.get();
auto readingThread = std::async( std::launch::async, loadFunc );
auto readVal = readingThread.get();
if( readVal != ttt ) {
std::cout << "mismatch!\t" << ttt << "\t!=\t" << readVal << "\n";
return 1;
}
}
std::cout << "done.\n";
return 0;
}
在可移植线程平台通常为您提供指定内存可见性或放置显式内存屏障的能力之前,可移植同步仅通过显式同步(如互斥)和隐式同步来完成。
一般情况下,在创建线程之前,都会设置一些线程启动时会访问的数据结构。为了避免仅仅为了实现这种通用模式而必须使用互斥锁,线程创建被定义为隐式同步事件。加入一个线程然后查看它计算的一些结果同样常见。同样,为了避免仅仅为了实现这种通用模式而必须使用互斥体,加入线程被定义为隐式同步事件。
由于线程创建和结构被定义为同步操作,加入线程必然发生在线程终止之后。因此,您会看到在线程终止 之前必然发生的任何事情。更改一些变量然后创建线程的代码也是如此——新线程必然会看到在创建它之前发生的所有更改。线程创建或终止时的同步就像互斥锁上的同步。同步操作创建了这种确保内存可见性的排序关系。
正如 SergeyA 所提到的,绝对不要试图通过测试来证明多线程世界中的某些东西。当然,如果测试失败,那就证明你不能依赖你测试的东西。但是,即使测试通过您能想到的所有测试方式都成功了,这并不意味着它不会在您未测试的某些平台、CPU 或库上失败。你永远无法通过那种测试来证明这样的东西是可靠的。
如果你想测试这样的东西,你可以使用模型检查器来探索测试用例的所有可能执行(受一些深奥的限制)。
参见 http://plrg.eecs.uci.edu/c11modelchecker.html
假设在 C++11 程序中,我们有一个名为 A 的主线程,它启动一个名为 B 的异步线程。在线程 B 中,我们对具有 std::memory_order_relaxed
内存顺序的原子变量执行原子存储。然后线程 A 与线程 B 连接。然后线程 A 启动另一个名为 C 的线程,该线程以 std::memory_order_relaxed
内存顺序执行原子加载操作。难不成线程C加载的内容和线程B写的内容不一样?换句话说,这里宽松的内存一致性是否会延伸到线程生命周期之后?
为了尝试这个,我写了一个简单的程序,运行 尝试了很多次。该程序不会报告不匹配。我在想,因为线程 A 在线程的启动中强加了一个顺序,所以不匹配不会发生。但是,我不确定。
#include <atomic>
#include <iostream>
#include <future>
int main() {
static const int nTests = 100000;
std::atomic<int> myAtomic( 0 );
auto storeFunc = [&]( int inNum ){
myAtomic.store( inNum, std::memory_order_relaxed );
};
auto loadFunc = [&]() {
return myAtomic.load( std::memory_order_relaxed );
};
for( int ttt = 1; ttt <= nTests; ++ttt ) {
auto writingThread = std::async( std::launch::async, storeFunc, ttt );
writingThread.get();
auto readingThread = std::async( std::launch::async, loadFunc );
auto readVal = readingThread.get();
if( readVal != ttt ) {
std::cout << "mismatch!\t" << ttt << "\t!=\t" << readVal << "\n";
return 1;
}
}
std::cout << "done.\n";
return 0;
}
在可移植线程平台通常为您提供指定内存可见性或放置显式内存屏障的能力之前,可移植同步仅通过显式同步(如互斥)和隐式同步来完成。
一般情况下,在创建线程之前,都会设置一些线程启动时会访问的数据结构。为了避免仅仅为了实现这种通用模式而必须使用互斥锁,线程创建被定义为隐式同步事件。加入一个线程然后查看它计算的一些结果同样常见。同样,为了避免仅仅为了实现这种通用模式而必须使用互斥体,加入线程被定义为隐式同步事件。
由于线程创建和结构被定义为同步操作,加入线程必然发生在线程终止之后。因此,您会看到在线程终止 之前必然发生的任何事情。更改一些变量然后创建线程的代码也是如此——新线程必然会看到在创建它之前发生的所有更改。线程创建或终止时的同步就像互斥锁上的同步。同步操作创建了这种确保内存可见性的排序关系。
正如 SergeyA 所提到的,绝对不要试图通过测试来证明多线程世界中的某些东西。当然,如果测试失败,那就证明你不能依赖你测试的东西。但是,即使测试通过您能想到的所有测试方式都成功了,这并不意味着它不会在您未测试的某些平台、CPU 或库上失败。你永远无法通过那种测试来证明这样的东西是可靠的。
如果你想测试这样的东西,你可以使用模型检查器来探索测试用例的所有可能执行(受一些深奥的限制)。
参见 http://plrg.eecs.uci.edu/c11modelchecker.html