繁体   English   中英

是否安全地解除引用不同线程中原子对象的READ ONLY非原子指针?

[英]Is dereferencing a READ ONLY non-atomic pointer to an atomic object in different threads safe?

如果我写这样的东西:

std::atomic<bool> *p = new std::atomic<bool>(false); // At the beginning of the program  

//...  

void thread1()  
{  
    while (!(*p))  
        // Do something  
}  

//...  

void thread2()  
{  
    //...  
    *p = true;  
    //...
}  

thread1thread2将同时运行。 p的值从未初始化,因此永远不会改变。 在这种情况下,取消引用操作是否安全? 出于性能原因,我想避免使用原子指针。

是的,这是安全的。 如果没有至少一个线程修改共享变量,则无法进行数据竞争。 由于两个线程都没有修改p ,所以没有比赛。

您发布的代码和问题是两个不同的事情。

代码将起作用,因为您取消引用非原子指针。 您取消引用std::atomic<bool>*将导致(运算符重载)顺序一致的提取/存储。 这可能不是必要的效率(大多数时候这样的标志用于释放操作),但它是安全的。

否则,只要没有其他线程修改数据,解除引用任何 (包括原子变量)的有效非原子指针是安全的。

取消引用非原子指针与写入它的另一个线程仍然是“安全的”,因为它不会崩溃。 然而,没有正式保证记忆没有乱码(对于对齐的POD,由于处理器如何访问存储器而有非常实用的保证​​),但更重要的是,在没有存储器排序保证的情况下它是不安全的 使用这样的标志时,通常会做这样的事情:

do_work(&buf); // writes data to buf
done = true;   // synchronize

这适用于一个线程,但不保证在并发时正常工作。 为此,您需要事先保证。 否则,在实现对数据的写入之前,另一个线程可能会获取对标志的更新。

解除引用(即:读取地址) 在intel架构上是原子的。 此外,由于不变,我猜它不仅在Intel / AMD上是正确的。 不过请看这篇文章了解更多信息。

澄清:在其他体系结构中,当写入地址时,如果只修改了部分地址,则可以切换线程,因此其他线程读取的地址将无效。

对于Intel, 如果地址在内存中对齐,则不会发生这种情况。

此外,由于*pstd::atomic<bool> ,它已经实现了所有需要的东西(native,asm,memory fences)。

这取决于你的两次访问是什么。 如果主设备在设置布尔值之前写入一些数据,则从设备需要一个内存屏障以确保它不会在布尔值之前读取所述数据。

也许现在你的线程只是等待这个布尔值退出,但是如果有一天你决定主机应该,例如,将终止状态传递给从机,你的代码可能会中断。
如果你在6个月后回来并修改这段代码,你确定你会记得你的从属循环之外的区域是一个无共享读区域,而你掌握布尔之前的区域是一个非共享写区域?

无论如何,你的布尔值需要是易变的,否则编译器可能会优化它。 或者更糟糕的是,你的同事的编译器可能会,而你将不再存在另一段不可靠的代码。

众所周知,volatile变量通常不足以进行线程同步,因为它们没有实现内存障碍,如下例所示:

主人:

// 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...

(如果主站的总线写入无序排列,则会出现对称问题)

当然,您可能永远不会在您的特定台式计算机上看到如此罕见的事件,就像您可能偶然运行一个程序破坏其自身的内存而不会崩溃。

然而,使用这种泄漏同步编写的软件正在勾选时间炸弹。 在一系列总线架构上编译并运行它们,并且有一天...... Ka-boom!


作为事实上,C ++ 11被允许创建一样,如果有什么给它的任务伤害多处理器编程了很多 ,并在同一时间提供什么,但糟糕的原子能,互斥体和条件变量处理同步(与当然是血腥尴尬的未来。

同步任务(尤其是工作线程)的最简单和最有效的方法是让它们在队列上处理消息。 这就是驱动程序和实时软件的工作方式,除非出现一些非凡的性能要求,否则任何多处理器应用程序都应该如此。

强制程序员用美化的标志来控制多任务是很愚蠢的 您需要非常清楚地了解硬件如何与原子计数器一起使用。
C ++的迂腐集团再次迫使每个人和他的狗成为另一个领域的专家,只是为了避免编写糟糕,不可靠的代码。

像往常一样,你会让大师们用一种放纵的微笑喷出他们的“良好做法”,而人们在破碎的自制队列中愚蠢的旋转循环中燃烧兆焦耳的CPU能力,相信“无等待”同步是阿尔法和欧米茄效率。

而这种表现迷恋是一个非问题。 “阻塞”调用只消耗可用计算能力的碎屑,而且还有许多其他因素会使性能比操作系统同步原语高出几个数量级(缺少标准方法来定位任务)给定处理器,一开始)。

考虑你的thread1奴隶。 访问一个原子bool将把一把沙子扔进公交缓存齿轮,使这个特殊访问速度减慢大约20倍。浪费了几十个周期。 除非你的奴隶只是在循环中弄乱它的虚拟拇指,否则这几个循环将相形见绌,单个循环将持续数千或数百万。 此外,如果你的奴隶完成工作而其兄弟奴隶不在,会发生什么? 它会在这个标志上无用地旋转并浪费CPU,还是阻塞任何互斥锁?
这正是为了解决消息队列被发明的这些问题。

像消息队列读取这样的适当OS调用可能会消耗几百个周期。 所以呢?
如果您的从属线程只是增加3个计数器,那么您的设计是错误的。 你没有启动一个线程来移动几个火柴棍,就像你没有为每个字节分配你的内存字节一样,即使是像C ++这样的高级语言。

如果您不使用线程来修复面包屑,您应该依赖简单且经过验证的机制,例如等待队列或信号量或事件(因缺少便携式解决方案而选择posix或Microsot),并且您不会注意到对性能的任何影响任何。

编辑 :更多关于系统调用开销

基本上,对等待队列的调用将花费几微秒。

假设您的普通工作人员数量为10到100毫秒,系统调用开销将无法从背景噪声中获得,并且线程终止响应将保持在可接受的限制范围内(<0.1 s)。

我最近实现了一个Mandelbrot set explorer作为并行处理的测试用例。 它绝不代表所有并行处理案例,但我仍然注意到一些有趣的事情。

在我的I3 Intel 2核/ 4个CPU @ 3.1 GHz上,每个CPU使用一个工作器,我测量了纯计算并行化的增益因子(即使用1个核心超过4个核心的执行时间的比率)(即没有任何数据依赖性)工人之间)。

  • 在每个核心上本地化线程(而不是让OS调度程序将线程从一个核心移动到另一个核心)将比率从3.2提高到3.5(理论上最大值为4)

  • 除了将线程锁定到不同的核心之外,最值得注意的改进是由于算法本身的优化(更有效的计算和更好的负载平衡)。

  • 用于让4名工作人员从公共队列中抽取的大约1000个C ++ 11互斥锁的成本达到7毫秒,即每次调用7微秒。

我很难想象高性能设计每秒执行超过1000次同步(或者你的时间可能更好地用于改进设计),所以基本上你的“阻塞”调用成本远低于1%的可用功率。相当低廉的PC。
选择是你的,但我不确定从一开始就实施原始原子对象将是性能的决定性因素。

我建议从简单的队列开始并做一些基准测试。 您可以使用pthread posix接口,或者将此非常好的示例作为转换为C ++ 11的基础。

然后,您可以调试程序并在同步无错误的环境中评估算法的性能。

如果队列被证明是真正的CPU占用并且您的算法无法重构以避免过多的同步调用,那么切换到您认为更高效的任何自旋锁应该相对容易,特别是如果您的计算已经简化并且数据依赖性已经排序事先出去。

PS :如果这不是商业秘密,我很乐意听到更多关于你的算法的消息。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM