10

一个相当基本的问题,但我没有看到它在任何地方被问到。

假设我们有一个全局结构(在 C 中),如下所示:

struct foo {
  int written_frequently1;
  int read_only;
  int written_frequently2;
};

我似乎很清楚,如果我们有很多线程读写,我们需要一个信号量(或其他锁)在written_frequently成员上,即使是读取,因为我们不能 100% 确定这个结构的分配是原子的.

如果我们想要很多线程来读取read_only成员,而没有线程来写入,那么我们是否需要在结构访问上设置一个信号量来读取?

(我倾向于说不,因为不断更改之前和之后的位置这一事实不应该影响read_only成员,并且读取该值的多个线程不应该相互干扰。但我不确定。 )


[编辑:我现在意识到我应该更好地问这个问题,以便非常具体地澄清我的意思。当然,当我第一次问这个问题时,我并没有真正理解所涉及的所有问题。当然,如果我现在全面编辑问题,我会毁掉所有这些好答案。我的意思更像是:

struct bar {
  char written_frequently1[LONGISH_LEN];
  char read_only[LONGISH_LEN];
  char written_frequently2[LONGISH_LEN];
};

我问的主要问题是,由于这些数据是结构的一部分,它是否会受到其他结构成员的影响,是否会反过来影响它们?

事实上,成员是整数,因此写入可能是原子的,在这种情况下实际上只是一个红鲱鱼。]

4

10 回答 10

7

您需要一个互斥锁来保证操作是原子的。因此,在这种特殊情况下,您可能根本不需要互斥锁。 具体来说,如果每个线程写入一个元素并且写入是原子的,并且新值独立于任何元素(包括自身)的当前值,则没有问题。

示例:多个线程中的每一个都更新一个“last_updated_by”变量,该变量仅记录更新它的最后一个线程。显然,只要对变量本身进行原子更新,就不会出现错误。


但是,如果线程一次读取或写入多个元素,您确实需要一个互斥体来保证一致性,特别是因为您提到锁定一个元素而不是整个结构

示例:线程更新结构的“日”、“月”和“年”元素。这必须以原子方式发生,以免另一个线程在“月”增量之后但在“日”换行到 1 之前读取结构,以避免诸如 2 月 31 日之类的日期。请注意,在读取时必须遵守互斥锁;否则您可能会读取错误的、半更新的值。

于 2008-11-05T17:43:25.957 回答
6

如果 read_only 成员实际上是只读的,则没有数据被更改的危险,因此不需要同步。这可能是在线程启动之前设置的数据。

无论频率如何,您都需要对可以写入的任何数据进行同步。

于 2008-11-05T16:30:46.727 回答
4

“只读”有点误导,因为变量在初始化时至少被写入一次。在这种情况下,如果初始写入和后续读取位于不同的线程中,您仍然需要在它们之间设置内存屏障,否则它们可能会看到未初始化的值。

于 2008-11-05T17:14:48.183 回答
3

读者也需要互斥锁!

似乎有一个普遍的误解,即互斥锁仅供编写者使用,而读者不需要它们。 这是错误的,并且这种误解是导致极难诊断的错误的原因。

这就是为什么,以示例的形式。

想象一个时钟每秒更新一次,代码如下:

if (++seconds > 59) {        // Was the time hh:mm:59?
   seconds = 0;              // Wrap seconds..
   if (++minutes > 59)  {    // ..and increment minutes.  Was it hh:59:59?
     minutes = 0;            // Wrap minutes..
     if (++hours > 23)       // ..and increment hours.  Was it 23:59:59?
        hours = 0;           // Wrap hours.
    }
}

如果代码不受互斥体保护,则另一个线程可以在更新过程中读取hoursminutes和变量。seconds按照上面的代码:

[午夜前开始] 23:59:59
[WRITER 增加秒数] 23:59:60
【WRITER换秒】23:59:00
[WRITER 增加分钟数] 23:60:00
[WRITER 总结] 23:00:00
[WRITER 增加小时数] 24:00:00
[作家结束时间] 00:00:00

从第一次递增到六步后的最后一次操作,时间无效。如果读者在此期间检查时钟,它将看到一个不仅不正确而且非法的值。而且由于您的代码可能依赖于时钟而不直接显示时间,因此这是众所周知的难以追踪的“弹跳”错误的典型来源。

修复很简单。

用互斥锁包围时钟更新代码,并创建一个读取器函数,该函数在互斥锁执行时也会锁定它。现在读取器将等待更新完成,写入器不会在读取过程中更改值。

于 2008-11-06T04:11:24.973 回答
2

不。

通常,您需要信号量来防止对资源的并发访问(int在本例中为)。但是,由于该read_only成员是只读的,因此在访问之间/期间它不会更改。请注意,它甚至不必是原子读取——如果没有任何变化,你总是安全的。

read_only你最初是如何设置的?

于 2008-11-05T16:35:41.163 回答
1

如果所有线程都只是读取,则不需要信号量。

于 2008-11-05T16:30:30.427 回答
1

您可能会喜欢阅读任何一篇关于实用无锁编程的论文,或者只是剖析和理解所提供的片段。

于 2009-03-17T12:09:02.603 回答
0

我会将每个字段隐藏在函数调用后面。只写字段将有一个信号量。只读只返回值。

于 2008-11-05T16:31:33.777 回答
0

添加到以前的答案:

  1. 在这种情况下,自然同步范式是互斥的,而不是信号量。
  2. 我同意您在只读变量上不需要任何互斥锁。
  3. 如果结构的读写部分具有一致性约束,则通常需要一个互斥体来处理所有这些,以保持操作的原子性。
于 2008-11-05T16:47:32.513 回答
0

非常感谢所有出色的回答者(以及所有出色的答案)。

总结一下:

如果结构中有一个只读成员(在我们的例子中,如果值被设置一次,早在任何线程可能想要读取它之前),那么读取这个成员的线程不需要锁、互斥锁、信号量或任何其他并发保护。

即使经常写入其他成员也是如此。不同的变量都是同一个结构的一部分这一事实没有区别。

于 2009-10-27T10:59:49.097 回答