我需要为读取操作使用互斥锁吗?

56

我有一个类,其中包含一个状态(一个简单的枚举),并且由两个线程访问。为了更改状态,我使用了一个互斥锁(boost::mutex)。在此情况下,检查状态(例如比较state_ == ESTABLISHED)是否安全?换句话说,当我只想读取可能被另一个线程并发写入的变量时,我需要使用互斥锁吗?

6个回答

28

这要看情况。

C++语言本身没有关于线程或原子性的说明。

但在大多数现代CPU上,读取整数是一种原子操作,这意味着即使没有互斥量,您仍将始终读取一致的值。

然而,没有互斥量或其他形式的同步,编译器和CPU可以自由地重新排列读取和写入,因此在一般情况下,任何更复杂的操作,涉及访问多个变量的操作仍然是不安全的。

假设写入线程更新了一些数据,然后设置一个整数标志以通知其他线程数据可用,这可能会重新排序,从而在更新数据之前设置标志。除非您使用互斥量或另一种内存屏障。

因此,如果您想要正确的行为,您并不需要像这样的互斥量,如果另一个线程在您读取它时写入变量,则没问题,它将是原子性的,除非你在非常不寻常的CPU上工作。但是您确实需要某种形式的内存屏障来防止编译器或CPU重新排序。


除非您指定为volatile,否则读取(或写入)可能永远不会执行。迟早会来是不够的。 - EFraim
4
即使使用volatile关键字,CPU或编译器仍可能重新排序写操作,使它们变得毫无意义。正确的解决方案是使用内存屏障,此时volatile只是一种无用的非最优化选择。 - jalf
@jalf:不,如果你只需要一个单独的标志。再次阅读问题。 - EFraim
1
很可能像你提出的解决方案会导致大量膨胀的代码。不,对于一个单一标志,你并不需要屏障。 - EFraim
3
但如果正在检查旗标以确定应用程序的下一步操作,那么:(a) 他需要在线程之间共享其他数据。 (b) 旗标检查是无意义的,他可以始终执行下一步操作。假设(a)是情况,"其他数据" 可能不会准备就绪,仅因为设置了旗标变量。假设这是由于旗标引起的可能会导致在调试时运行崩溃而在发布版本中崩溃。 - peter karasev

10

您有两个线程,它们交换信息,是的,您需要一个互斥锁,您可能还需要一个条件等待。

在您的示例中(比较state_ == ESTABLISHED),表示线程2正在等待线程1启动连接/状态。如果没有互斥锁或条件/事件,线程2必须持续轮询状态。

线程用于提高性能(或改善响应性),轮询通常会导致性能降低,要么通过消耗大量CPU,要么引入延迟由于轮询间隔。


1
+1 建议使用条件变量。听起来他有一个线程需要响应另一个状态的改变。如果是这种情况,条件变量更为适合。 - Falaina
@Ermelli 如果我们仍然想使用忙等待循环(因为循环期间可能会做其他事情),那么我们是否仍需要互斥锁? - Nick

2

是的。如果线程a在线程b写入变量时读取该变量,则可能读取到未定义的值。读取和写入操作不是原子操作,特别是在多处理器系统上。


2
当一个写线程执行(获取->写入->存储)时,我不会看到读取器在其中间获取未定义的值,无论是先前的还是后续的,但永远不会是未定义的。 - Arkaitz Jimenez
1
考虑在一条指令中读取的枚举值。 - Arkaitz Jimenez
1
@Arkaitz:undefined 可能不是正确的词。但是随着缓存层数的增加、延迟时间的增加等,CPU/内存架构变得越来越复杂。答案很简单:对于无锁共享数据,说“不”!即使专家在这个领域也会犯很多错误。 - sellibitze

2

一般来说,如果您的变量已经使用“volatile”声明,那么您不需要这样做。但是,只有在它是单个变量时才这样做 - 否则,您应该非常小心可能出现的竞争。


你认为volatile为什么很重要? - jalf
2
@jalf:volatile 告诉编译器不要执行可能导致代码看到变量的旧副本的优化。 - Stephen C
2
但问题并不是关于过时的副本。即使变量不是易失性的,它迟早也会被写出。无论易失性如何,它都是原子的,并且易失性不能防止加载/存储重排序。因此,在这种情况下,易失性实际上并没有为您带来任何好处。它没有解决需要解决的问题,而且解决了一个本来已经解决的问题。 - jalf
1
是的,这就是为什么正确的解决方案应该使用内存屏障而不是 volatile。在多线程上下文中,volatile 基本上是无用的。它会减慢代码速度,而不提供你所需的保证。 - jalf
@jalf:不,这并不是无用的。有界等待与无限等待是区别所在。你知道自旋锁通常基于易失性吗? - EFraim
显示剩余2条评论

2
实际上,没有理由将对对象的读取访问锁定。您只需要在写入对象时锁定它。这正是读写锁所做的。只要没有写操作,它就不会锁定对象。它提高了性能并防止死锁。请参阅以下链接以获取更详细的说明: 维基百科 CodeProject

只要没有写操作,它就不会锁定对象。但是如果您不锁定读取操作,如何知道是否发生了写操作?在您提供的维基百科页面中,伪代码实现确实锁定了读取器,以便读取器正在读取或写入器正在写入,但两者不能同时进行。 - jmmut

0

枚举类型的访问(读或写)应该受到保护。

另外一件事: 如果线程争用较少且线程属于同一进程,则关键段比互斥量更好。


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