我需要保护这个变量吗?需要使用锁吗?

10

我在一个多处理器机器上有一个C++中的布尔类型变量。变量一开始是true,然后有一些线程,任意一个或多个线程可能会将其写为false。

同时,这些线程也可能读取这个变量来检查它的状态。我不关心读取该变量是否与任何写入同步,它们每个都发生在代码的不同位置,并且无论它在任何特定的写入之前还是之后,都没有关系。那么,我需要为这个布尔值加锁吗?

我只有在非常低层次上需要锁,即如果内存可能因两个竞争的写入而受到破坏。例如,处理器A上的汇编指令正在向表示布尔值的字节写入0,同时处理器B也在做同样的事情……而内存最终不是写入了0,而是值22或其他某些东西。那会对某些事情造成麻烦。

所以,通常情况下,如果处理器A正在将3写入内存位置,而处理器B正在写入7,没有同步,我能否保证最终至少得到3或7其中之一?还是说打破内存太容易了?

编辑:

谢谢大家的评论。以下是更多信息:程序中当然有同步机制。总之,被讨论的标志表示某个内存池是否“脏”(需要压缩)。因此,任何线程都可以决定将此标志设置为false(表示该池是脏的)。例如,从池中释放内存会使其变脏。然后,任何线程也可以读取此标志并设置另一个标志以表示需要进行清理——如果我们的内存不足,则在从池中分配内存时执行此检查。在我的主要关键部分之间的迭代中,在每个线程寻找更多要处理的数据时,我将使线程检查此第二个标志,并执行适当的操作,以确保所有其他线程完成当前迭代,一个线程清理内存,将第一个标志设置回true(即池不脏了),将第二个标志设置回false,然后再次释放所有线程。

我认为我不需要锁,因为锁可以确保写操作不会与另一个写操作或读操作同时发生。但是谁在乎呢,只要硬件不让我失望,最坏情况是读操作随机在写操作之前或之后发生--如果我用锁来保护它,这个结果也是一样的,只不过那时我们会更加确信它在写操作之前或之后发生了...

我认为同样的论点适用于我上面提到的第二个标志。


1
出于好奇,为什么您不关心读取操作呢?假设某个线程已经写入了 false - 那么如果您不在意读取结果是 true 还是 false,那么为什么还要进行读取操作呢?难道只是因为“错误”的 true 读取是无害的吗? - Steve Jessop
6个回答

10
在大多数通用硬件上,单个字的读写是原子性的,因此,如果两个或多个竞争的写操作(和读取操作)针对同一内存位置,不会破坏值。这里重要的是CPU之间的缓存一致性
同样,在通用硬件上,您可以通过将单个布尔变量标记为volatile(已被宣布对并发编程无用)来防止编译器将其优化为寄存器,但前提是您真正不关心写入的顺序。
让我用一个检查清单重申一下:
  • 您准备好丢失一些对该布尔变量的更新吗?
  • 您确定源代码中在布尔翻转之前但可能会在之后重新排序的其他内存更新不会搞乱事情吗?
  • 您确定您不关心应用程序中事件的顺序吗?
如果您有三个强烈的“是”的答案,您可能可以避免保护该标志。仍然需要考虑在读取变量之前插入获取内存屏障,在写入变量之前插入释放内存屏障。我的建议是重新思考设计,并明确线程间的同步通信和事件顺序。希望这有所帮助。

  1. 你说失去对布尔值的更新……你是指硬件会忽略写入吗?我不这么认为,所以我不确定你的意思(你可以看到我上面的补充说明)
  2. 是的
  3. 有些事情我很在意。请注意,我从未在同一段代码块中同时读取和写入此标志。始终只读取一次或写入一次。——这基本上就是为什么同步在这里似乎毫无用处的原因。
我认为我们找到了volatile的用途! :)
- Scott
还要注意的是,我在这里没有建模“等待…”场景。我从不希望线程等待false或等待任何东西。加油! - Scott
你提到了缓存一致性,但并没有说它的重要性在哪里。除非你在使用某些相当奇特的硬件,否则可以假设缓存一致性确保一个核心写入的任何内容都能被其他核心看到。此外,volatile确实可以防止与其他volatile对象 重新排序。@Scott: 不,你找到了使用内存屏障的好地方,而不是使用volatile的地方。 - jalf
@Scott,硬件不会忽略写入操作,但你的线程可能会覆盖彼此的更新。 @jalf,关于缓存一致性的阅读留给读者自己练习 :) - Nikolai Fetissov

8
如果您只是检查变量的状态并将其设置为false,那么除了一些线程可能稍微晚一些看到变量已经设置为false之外,就没有什么好担心的了。(通过使用“volatile”关键字可以在某种程度上克服这个问题。)然后两个线程可能会将其设置为false,这不是问题,因为变量在一个方向上被设置为单个值。假设布尔值写入内存位置不能保证是原子性的,那么有什么危害呢?它们都将写入相同的最终值。
但是,如果:
- 值的设置不仅限于单向:您将其设置为false,然后再设置为true,然后再设置为false等等。 - 您对线程设置值为false的信息采取了一些依赖行动。因为显然可能会有两个获胜者。
在这种情况下,您必须使用锁定方法。

3

这是一个容易导致问题的方式。使用布尔值,大多数情况下可能还好,但没有提供任何保证。

你有两个选择:使用互斥锁(lock)或使用原子操作。原子操作将利用硬件指令以线程安全的方式进行测试和设置操作,而无需实际的互斥锁,是一种更轻量级的解决方案。GNU编译器通过架构特定的扩展提供对原子操作的访问。还有一些可移植的原子操作库,GLib C库提供回退到使用互斥锁的原子操作,如果原子操作不可用。虽然它是一个相当复杂的库,还具有许多其他功能。

Boost.Atomic库为C++抽象了原子操作;从它的名称来看,它似乎旨在被纳入Boost C++库集合中,但尚未实现。


1

对于布尔值,通常不需要使用互斥量。但是(如Michael E所指出的),任何事情都有可能发生,因此在做出这样的决定之前,您可能需要更多地了解您的架构。另一个要注意的问题是:如果在例程逻辑的过程中多次读取布尔值,则代码可能仍然需要围绕布尔值的整体逻辑添加锁。

以下是我阅读以保持多线程技能的一些博客:

链接

http://herbsutter.com/2009/04/20/effective-concurrency-use-thread-pools-correctly-keep-tasks-short-and-nonblocking/

此致敬礼,


1

您正在询问两件事情。

首先,您正在询问布尔赋值的原子性。没有保证布尔赋值将是原子操作。在实践中,它通常是这样的,但您不应该依赖于此。一些奇怪的架构可能会使用多个机器指令来实现布尔赋值...

其次,您正在询问由于并行写入而导致数据损坏的问题 - 在实践中,从CPU到内存的传输是通过总线完成的,该总线几乎总是包含比您正在处理的原始类型更多的位。因此,在非常奇怪的架构或处理大数字(系统不支持本地处理)时,可能会发生这种损坏。在实践中,您通常会得到3或7。但同样,您不能依赖它。

总之,您需要一个锁。


1

在大多数的商品硬件上,单词读写都不是原子操作;记住你这里有虚拟内存机器,其中任何一个操作都可能会导致页错误。

更一般地说,你可能会发现使用互斥锁作为例行事项要比纠结于这次是否可以脱离互斥锁来得容易和快速。在不必要的地方添加一个互斥锁并不会引起错误;而在必要的地方遗漏掉一个则可能会。


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