是否有必要锁定一个*只从一个线程写入*而*只从*另一个读取?

Cha*_*ani 24 c c++ arrays multithreading

我有两个线程在运行.他们共享一个阵列.其中一个线程向数组添加新元素(并删除它们),另一个使用此数组(仅限读取操作).在我添加/删除数组或从中读取数组之前,是否有必要锁定数组?

更多详情:

  • 我将需要在另一个线程中继续迭代整个数组.如前所述,那里没有写操作."只需扫描像固定大小的循环缓冲区"
  • 在这种情况下,最简单的方法是使用锁.然而,锁可能非常慢.如果可以避免使用锁,我不想使用锁.此外,从讨论中得出,可能没有必要(实际上不是)锁定阵列上的所有操作.只是锁定数组的迭代器管理(将由另一个线程使用的计数变量)就足够了

我不认为这个问题"过于宽泛".如果仍然如此,请告诉我.我知道这个问题并不完美.为了能够解决问题,我必须至少结合3个答案 - 这表明大多数人无法完全理解所有问题,并被迫做一些猜测工作.但大多数都是通过我试图纳入问题的评论得出的.答案帮助我非常客观地解决了我的问题,我认为这里提供的答案对于从多线程开始的人来说是非常有用的资源.

nos*_*sid 18

如果两个线程在同一个内存位置上执行操作,并且至少有一个操作是写操作,则会出现所谓的数据争用.根据C11和C++ 11,具有数据争用的程序的行为是未定义的.

因此,您必须使用某种同步机制,例如:

  • (仅为其他人澄清:)另请注意,这不仅适用于*同时*发生的操作.它也适用于顺序操作(线程1写入,然后一段时间后线程2读取).即使操作是顺序的,也需要某种同步机制(特别是为了确保CPU高速缓存等同步). (7认同)
  • @Cornstalks:实际上有两个重要的补充:1)至少有一个操作不是原子的; 2)操作之间没有发生关系. (4认同)

Sha*_*our 8

如果您从多个线程写入和读取相同位置,则需要执行锁定或使用原子.我们可以通过查看C11草案标准(C++ 11标准看起来几乎完全相同,等效部分1.10)来看到这一点.在5.1.2.4 多线程执行和数据竞赛一节中说明如下:

如果其中一个修改内存位置而另一个读取或修改相同的内存位置,则两个表达式评估会发生冲突.

和:

程序的执行包含数据竞争,如果它在不同的线程中包含两个冲突的动作,其中至少有一个不是原子的,并且都不会在另一个之前发生.任何此类数据争用都会导致未定义的行为.

和:

此标准通常会排除将引用分配给可能由抽象机器修改的潜在共享内存位置的编译器转换,因为在抽象机器执行不具备的情况下,这样的分配可能会覆盖另一个线程的另一个分配遇到了数据竞赛.这包括覆盖不同内存位置中相邻成员的数据成员分配的实现.在有问题的原子可能混淆的情况下,我们通常也会排除原子载荷的重新排序,因为这可能违反了"可见序列"规则.

如果你只是在数组中添加数据,那么在C++世界中,std :: atomic索引就足够了,因为你可以添加更多元素然后以原子方式递增索引.但是既然你想要增长和缩小数组,那么你将需要使用互斥锁,在C++世界中,std :: lock_guard将是一个典型的选择.


Mic*_*nda 5

回答你的问题:也许

简而言之,问题的框架方式并没有提供关于是否需要锁的足够信息。

在大多数标准用例中,答案是肯定的。这里的大多数答案都很好地涵盖了这种情况。

我将介绍另一种情况。

鉴于您提供的信息,您什么时候不需要锁?

这里还有一些其他问题可以帮助更好地定义是否需要锁,是否可以使用无锁同步方法,或者是否可以在没有显式同步的情况下逃脱。

写入数据会是非原子的吗?意思是,写入数据会导致“数据撕裂”吗?如果您的数据是 x86 系统上的单个 32 位值,并且您的数据是对齐的,那么您可能会遇到写入数据已经是原子的情况。可以安全地假设,如果您的数据的大小大于指针的大小(x86 上为 4 个字节,x64 上为 8 个字节),那么您的写入不能在没有锁的情况下是原子的。

数组的大小是否会以需要重新分配的方式发生变化?如果你的读者正在浏览你的数据,数据会不会突然“消失”了(内存被“删除”了)?除非您的读者考虑到这一点(不太可能),否则如果可以重新分配,您将需要锁定。

当您将数据写入数组时,读者是否“看到”了旧数据?

如果你的数据可以原子写入,你的数组不会突然不存在,读者可以看到旧数据......那么你就不需要锁了。即使满足这些条件,使用内置的原子函数进行读取和存储也是合适的。但是,在这种情况下,您不需要锁:)

使用锁可能最安全,因为您不确定问这个问题。但是,如果您想玩转不需要锁的边缘情况...就可以了:)


Use*_*ess 0

如果它是固定大小的数组,并且您不需要传达任何额外的内容,例如写入/更新的索引,那么您可以避免互斥,但读者可能会看到以下警告:

  • 根本没有更新
    • 如果您的内存排序足够宽松以至于发生这种情况,则需要在编写器中使用存储栅栏并在消费者中使用加载栅栏来修复它
  • 部分写入
    • 如果存储的类型在您的平台上不是原子的(int 通常应该是)
    • 或者您的值未对齐,特别是如果它们可能跨越缓存行

但这完全取决于您的平台 - 硬件、操作系统和编译器都会影响它。你还没告诉我们它们是什么。

可移植的 C++11 解决方案是使用atomic<int>. 您仍然需要决定需要哪些内存排序约束,以及这对平台上的正确性和性能意味着什么。

  • -1 如果没有正确的同步,读取器可能*永远不会*看到写入的值。 (5认同)