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

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

更多详情:

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

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

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

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

  • 的std ::primefaces
  • 的std ::互斥

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

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

和:

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

和:

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

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

其中一个线程向数组添加新元素,另一个[读取]此数组

为了向数组添加元素或从数组中删除元素,您需要一个索引来指定存储有效数据的数组的最后位置。 这样的索引是必要的,因为如果没有潜在的重新分配(这是一个完全不同的故事),数组就无法resize。 您可能还需要第二个索引来标记允许读取的初始位置。

如果您有一个或两个这样的索引,并假设您从不重新分配数组,则只要您锁定有效索引的写入,就不必在写入数组本身时锁定。

 int lastValid = 0; int shared[MAX]; ... int count = toAddCount; // Add the new data for (int i = lastValid ; count != 0 ; count--, i++) { shared[i] = new_data(...); } // Lock a mutex before modifying lastValid // You need to use the same mutex to protect the read of lastValid variable lock_mutex(lastValid_mutex); lastValid += toAddCount; unlock_mutex(lastValid_mutex); 

这样做的原因是,当您在锁定区域外执行对shared[]写入时,阅读器不会“查看”超过lastValid索引。 写入完成后,锁定互斥锁,这通常会导致刷新CPU缓存,因此在允许读取器查看数据之前,对shared[]的写入将完成。

回答你的问题: 也许吧

简单地说,问题框架的方式没有提供关于是否需要锁定的足够信息。

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

我将介绍另一个案例。

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

此处还有一些其他问题可以帮助您更好地定义是否需要锁定,是否可以使用无锁同步方法,或者是否可以避免显式同步。

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

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

当您向arrays写入数据时,如果读者“看到”旧数据,这样可以吗?

如果您的数据可以primefaces方式编写,那么您的数组就不会突然出现,并且读者可以查看旧数据……然后您就不需要锁定了。 即使满足这些条件,使用内置的primefaces函数进行读取和存储也是合适的。 但是,这是一个你不需要锁的情况:)

可能最安全的使用锁,因为你不确定这个问题。 但是,如果你想玩你不需要锁的边缘情况……你去:)

锁? 不。但你确实需要一些同步机制。

您所描述的内容听起来像是一个“SPSC”(Single Producer Single Consumer)队列,其中有大量无锁实现,包括Boost.Lockfree中的一个

这些工作的一般方式是在封面下面有一个包含对象和索引的循环缓冲区。 编写器知道它写入的最后一个索引,如果它需要写入新数据,它(1)写入下一个槽,(2)通过将索引设置为前一个槽+ 1来更新索引,然后(3)向读者发出信号。 然后读者读取,直到它遇到一个不包含它所期望的索引的索引并等待下一个信号。 删除是隐式的,因为缓冲区中的新项目会覆盖以前的项目。

您需要一种primefaces方式更新索引的方法,该索引由atomic <>提供并具有直接硬件支持。 你需要一种方法让作家向读者发出信号。 您还可能需要内存防护,具体取决于平台st(1-3)按顺序发生。 你不需要像锁一样沉重的东西。

“经典”POSIX确实需要锁定这种情况,但这是矫枉过正。 您只需确保读取和写入是primefaces的。 自2011年版本的标准以来,C和C ++都使用该语言。 编译器开始实现它,至少最新版本的Clang和GCC拥有它。

这取决于。 可能不好的一种情况是,如果要删除一个线程中的项目,然后通过读取线程中的索引读取最后一个项目。 那个读线程会抛出一个OOB错误。

据我所知,这正是锁的用例。 同时访问一个数组的两个线程必须确保一个线程已准备就绪。 如果线程A没有完成工作,线程B可能会读取未完成的数据。

如果它是一个固定大小的数组,并且您不需要像编写/更新的索引那样进行任何额外的通信,那么您可以通过读者可能看到的警告来避免相互排斥:

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

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

可移植的C ++ 11解决方案是使用atomic的数组。 您仍然需要确定所需的内存排序限制,以及这对平台上的正确性和性能意味着什么。

如果你为你的数组使用例如vector (这样它可以动态增长),那么在写入期间可能会发生重新分配,你输了。

如果您使用大于始终写入的数据条目并以primefaces方式读取(几乎任何复杂的数据类型),则会丢失。

如果编译器/优化器决定在某些操作期间将某些东西保留在寄存器中(例如计数器中保存数组中的有效条目数),则会丢失。

或者即使编译器/优化器决定切换数组元素分配和计数器增量/减量的执行顺序,也会丢失。

所以你确实需要某种同步。 这样做的最佳方法是什么(例如,仅锁定数组的某些部分可能值得),这取决于您的具体情况(线程访问数组的频率和模式)。