线程安全问题

4

一个简单的情况,如果我有三个线程,其中一个用于窗口应用程序,并且我希望当窗口应用程序关闭时它们都停止运行,那么如果我使用一个全局变量,那么只有当全局变量为真时,这三个线程才会停止,否则它们将继续工作,这种情况下是否是线程安全的?
在C++编程中,volatile关键字是否有帮助?

5个回答

4
理论上,仅使用 volatile 是不够的。存在两个抽象层:
  • 源代码行为和实际操作码之间;
  • 一个核/处理器看到的与其他核/处理器看到的之间。
编译器可以在寄存器中缓存数据并重新排序读取和写入。使用 volatile 可以指示编译器按照源代码中指定的顺序执行读取和写入操作生成操作码。但这只处理了第一层。管理处理器核心之间通信的硬件系统也可能延迟和重新排序读取和写入。
碰巧在 x86 硬件上,核心将写入内容快速传播到主内存,并且其他核心自动被通知内存已更改。因此, volatile 看起来是足够的:它确保编译器不会在寄存器中进行奇怪的操作,并且内存系统足够友好,可以从那一点开始处理事情。请注意,这在所有系统上都不是真的(我认为至少有一些 Sparc 系统可以延迟任意时间的写入传播,可能是几个小时),我曾在 AMD 的手册中读到过,AMD 明确保留了在某些未来的处理器中稍后传播写入的权利。
因此,干净的解决方案是在访问全局变量(读取和写入)时使用互斥锁(Unix 上的 pthread_mutex_lock(),Windows 上的 EnterCriticalSection())。互斥原语包括一种特殊操作,称为“内存屏障”,它类似于增强版的 volatile (它对两个抽象层都起作用)。

如果您的内存系统在缓存一致性方面出现问题,那么在使用volatile时,编译器是否有责任插入适当的代码来强制缓存保持一致呢? - Martin York
内存屏障的代价很高,因此编译器对使用它们持谨慎态度。volatile 最初是为了与内存映射 I/O 设备和信号处理程序(信号处理程序是异步的,但发生在同一个核心上)进行交互而设计的。有很多代码使用 volatile 进行操作,并且会受到带屏障的 volatile 的惩罚。volatile 不是为了成为屏障而设计的。请注意,其他语言的做法不同。Java 的 volatile 包括内存屏障(甚至确保原子读写,即使是像 longdouble 这样的“大”值类型)。 - Thomas Pornin

3

如果你只想从其他线程的共享变量中“读取”,那么在你描述的情况下是可以的。

是的,需要使用volatile提示,否则编译器可能会“优化掉”该变量。

等待线程完成(即join)也很好:这样,应用程序应进行的任何清理都有机会完成。


实际上只有窗口线程会在那里写入,当它退出时,将值更改为true。这样可以吗? - Daniel
1
你真的需要volatile关键字——它不仅仅是一个有用的提示。虽然处理器不会忽略另一个核心的访问(只是重新排序),但编译器如果检测到它无法改变,可能会完全删除变量引用-因为它在另一个线程中设置,所以它似乎没有发生改变。 - Eamon Nerbonne
我以为有些编译器足够聪明,能够检测到这种情况。就我个人而言,在这种情况下,我总是使用volatile提示...嵌入式软件背景有所帮助。 - jldupont
Volatile 是相当无意义的。它提供了你所需要的一半(内存访问不会被优化掉),但并非全部(它不能保证内存访问的顺序或写入何时可见)。相反,使用内存屏障,这才是它们的用途。 - jalf
@jalf: 在这种情况下,这有什么关系呢?我的意思是:内存屏障提供了什么值得这个努力的东西? - jldupont
显示剩余2条评论

1

不行,因为存在内存可见性问题,这是有风险的。在多处理器上,向一个处理器的内存写入并不意味着另一个处理器会立即看到该更改。此外,如果不使用互斥锁,可能需要相当长的时间才能将更改传播到其他处理器。


在x86上,以及据我所知,在每个共享内存架构下,远程写入将使本地缓存失效。因此,这很好用 - 你可能会稍微延迟看到“退出”标志,并且你可能会看到退出标志的写入与其他副作用重新排序,但最终你会看到它。 - Eamon Nerbonne
更大的风险是,如果变量不是易失性的,编译器甚至可能不会写入内存。 - erikkallen

0

是的,这是一种常见的技术。

但在主线程退出main()之前,您还应该等待所有子线程退出。
在大多数线程实现中,如果主线程退出main(),则所有当前活动的子线程都会被重复执行(请参阅您的线程文档以了解详细信息),而不允许它们的堆栈正确地展开。因此,RAII的所有好处都将丧失。

因此,设置全局变量,然后等待(大多数线程系统都有join方法,允许您等待(处于非忙碌状态的线程)安全退出)所有子线程干净地退出,然后才允许主线程退出。


那正是我计划要做的,将变量设置为true,并等待线程结束。感谢你的答案! - Daniel

0

只要您更改变量的值以使线程退出,它就是安全的。在此时,您需要1)同步访问,并且2)需要做一些(抱歉,易失性不足够)来确保新值被正确地传播到其他线程。

前者很容易。 后者则更加困难 - 到了几乎肯定需要使用某种库或操作系统提供的机制的程度。


1
Volatile应该足够了。它告诉编译器变量可能会从意外(或非确定性)的来源更新,因此它不能被缓存或优化掉。 - Martin York
1
@MartinYork - volatile可能不足以提供可见性语义:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html - R Samuel Klatchko
@Martin:Volatile基本上足以告诉编译器变量不能只停留在寄存器中。然而,它对于变量只停留在缓存中什么也做不了。一些硬件(例如x86)维护严格的缓存一致性,因此对它们来说这已经足够了。其他硬件则没有,对它们来说volatile是不够的。总的来说,volatile不是线程处理的一个很好的工具——在某些方面它过于限制,但在其他方面又不够限制。 - Jerry Coffin
1
@Martin:也许编译器应该包含刷新缓存的指令,但事实上,1)大多数编译器不会这样做,2)我没有看到标准要求它们这样做。我们不知道访问顺序是否会出现问题——在杀死其他线程之后,主线程将(完全正确地)被写入,假定不再需要同步访问。如果它所做的只是退出,那可能不是一个问题——但清理很可能涉及破坏共享的数据。如果另一个线程仍然活动,那将会引起问题。 - Jerry Coffin
@Jerry:使用与单线程应用程序完全相同的技术,我们实现了最终的缓存一致性。多线程应用程序的区别在于,您无法保证读/写的顺序(在一般情况下)(因此您无法保证确定性行为)。但在这种特定情况下,这没有任何影响,因为主线程必须等待所有子线程。 - Martin York
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接