这取决于您的两次访问周围的情况。如果 master 在设置 boolean 之前写入了一些数据,slave 需要一个内存屏障来确保它不会在 boolean 之前读取所述数据。
也许现在你的线程只是在等待这个布尔值退出,但是如果有一天你决定 master 应该,例如,将终止状态传递给 slave,你的代码可能会中断。
如果你 6 个月后回来修改这段代码,你确定你会记得你的从循环之外的区域是一个非共享读取区域,而你的主布尔更新之前的区域是一个非共享写入区域?
无论如何,您的布尔值必须是可变的,否则编译器可能会将其优化掉。或者更糟的是,您同事的编译器可能会在您编写另一段不可靠的代码时。
众所周知,易失性变量通常不足以用于线程同步,因为它们没有实现内存屏障,就像这个简单的例子一样:
主人:
// previous value of x = 123
x = 42;
*p = true;
从处理器上的总线逻辑:
write *p = true
奴隶:
while (!*p) { /* whatever */ }
the_answer = x; // <-- boom ! the_answer = 123
从属处理器上的总线逻辑:
write x = 42 // too late...
(如果 master 的总线写入被乱序调度,则会出现对称问题)
当然,您可能永远不会在您的特定台式计算机上看到如此罕见的情况,就像您可以偶然运行一个程序破坏自己的内存而不会崩溃一样。
尽管如此,使用这种泄漏同步编写的软件是定时炸弹。在一系列总线架构上编译并运行它们足够长的时间,有一天......Ka-boom!
事实上,C++11 正在伤害多处理器编程很多,它允许创建任务,就像它没有任何东西一样,并且在相同的情况下时间只提供蹩脚的原子、互斥锁和条件变量来处理同步(当然还有该死的尴尬未来)。
同步任务(尤其是工作线程)最简单、最有效的方法是让它们处理队列中的消息。这就是驱动程序和实时软件的工作方式,任何多处理器应用程序都应如此,除非出现一些非凡的性能要求。
强迫程序员用美化的标志来控制多任务处理是愚蠢的。您需要非常清楚地了解硬件如何使用原子计数器。
C++ 的学究派再次迫使每个人和他的狗成为另一个领域的专家,只是为了避免编写蹩脚、不可靠的代码。
和往常一样,你会看到大师们带着放纵的微笑大肆宣扬他们的“良好实践”,而人们则在破碎的自制队列中在愚蠢的旋转循环中消耗兆焦耳的 CPU 功率,相信“无需等待”同步是最重要的。效率的阿尔法和欧米茄。
这种对性能的痴迷不是问题。 “阻塞”调用只消耗可用计算能力的碎片,还有许多其他因素会损害性能,比操作系统同步原语高几个数量级(缺乏在给定的处理器,开始)。
考虑你的 thread1 从站。访问一个原子布尔值会将一把沙子扔到总线缓存齿轮中,使这种特定的访问速度减慢大约 20 倍。这浪费了几十个周期。除非你的奴隶只是在循环中玩弄它的虚拟拇指,否则这少数几个循环将与一个循环将持续的数千或数百万个相比相形见绌。
另外,如果你的奴隶完成了工作而它的兄弟奴隶没有工作,会发生什么?它会在这个标志上无用地旋转并浪费 CPU,还是阻塞在任何互斥体上?
消息队列正是为了解决这些问题而发明的。
像消息队列读取这样的正确操作系统调用可能会消耗几百个周期。那么呢?
如果您的从属线程只是在那里增加 3 个计数器,那么就是您的设计有问题。您不会启动线程来移动几根火柴,就像您不会为每个字节分配内存字节一样,即使是 C++ 这样的高级语言。
如果您不使用线程来咀嚼面包屑,您应该依赖简单且经过验证的机制,例如 等待 队列或信号量或事件(选择 posix 或 Microsot 的)由于缺乏便携式解决方案),您不会注意到对性能有任何影响。
编辑:更多关于系统调用开销
基本上,调用等待队列将花费几微秒。
假设您的平均工作人员处理数字 10 到 100 毫秒,系统调用开销将无法从背景噪音中分辨出来,并且线程终止响应将保持在可接受的范围内(
我最近实现了一个Mandelbrot set explorer 作为并行处理的测试用例。它绝不代表所有并行处理案例,但我仍然注意到一些有趣的事情。
在我的 I3 Intel 2 cores / 4 CPUs @3.1 GHz 上,每个 CPU 使用一个 worker,我测量了纯计算并行化(即没有工作人员之间的数据依赖关系)。
-
将线程分别本地化到一个内核上(而不是让操作系统调度程序将线程从一个内核移动到另一个内核)将比率从 3.2 提高到 3.5(超出理论最大值 4)
除了将线程锁定到不同的内核之外,最显着的改进是算法本身的优化(更高效的计算和更好的负载平衡)。
大约 1000 个 C++11 互斥锁用于让 4 个工作人员从一个公共队列中提取数据的成本为 7 毫秒,即每次调用 7 微秒。
我几乎无法想象每秒执行超过 1000 次同步的高性能设计(否则您的时间可能会更好地用于改进设计),因此基本上您的“阻塞”调用将花费远低于 1% 的功率在相当便宜的 PC 上可用。
选择权在你,但我不确定从一开始就实现原始原子对象是否会成为性能的决定性因素。
我建议从简单的队列开始并进行一些基准测试。您可以使用 pthread posix 接口,或者将 this pretty good sample 作为转换为 C++11 的基础。
然后,您可以在无同步错误的环境中调试程序并评估算法的性能。
如果队列被证明是真正的 CPU 占用者并且您的算法无法重构以避免过多的同步调用,那么切换到任何自旋锁应该相对容易假设效率更高,特别是如果您的计算已经被简化并且数据依赖关系已经预先整理好。
P.S:如果这不是商业机密,我会很高兴听到更多关于您的算法的信息。