【问题标题】:Multiple threads accessing one variable多个线程访问一个变量
【发布时间】:2015-07-10 23:02:57
【问题描述】:

我在我正在阅读的一本教科书中发现了这个问题。解决方案也在下面给出。我无法理解最小值如何为 2。为什么一个线程不能读取 0,而所有其他线程都执行并写入 1?而不管是1还是2,最后写的线程还必须完成自己的循环?

int n = 0;
int main(int argc, char **argv) {
 for (i = 0; i < 5; i++) {
 int tmp = n;
 tmp = tmp + 1;
 n = tmp;
 }
 return 0;
}

如果一个线程运行这个应用程序,你会期望最终 输出为 5。如果 5 个线程并行运行同一个循环会怎样?什么 n 可以有的最大值和最小值是多少?最大的应该 不言而喻:25,5 个线程的 5 个增量。然而, 推理最小的可能值更加困难。提示:n 可能小于 5,但由您自己找出原因。

解决方案:

五个线程运行这个五次迭代循环并且没有 防止并发访问,n 可以达到的最低值 是两个。工作时最容易理解如何达到这个结果 从最终结果倒退。为了最终输出为两个,a 线程必须从 n 中读取值 1,将其递增,然后 写了两个。这意味着另一个线程写了一个,这意味着 它最初也读为零(这也是 n 的起始值)。 这解释了五个线程中的两个线程的行为。然而, 要发生这种行为,其他三个线程的结果必须 已被覆盖。两个有效的处决可以实现这一点。 要么 1) 所有三个线程开始并完成之间的执行 第一个线程读取零并写入一个,或 2) 所有三个线程 在最后一个线程读取 1 和 写两个。两个执行顺序都有效。

【问题讨论】:

  • 嗯,这只会导致未定义的行为,在 C11 线程中,因为没有使用内存栅栏或原子。
  • @MattMcNabb 是的。我也应该包括第一部分。 “您已经看到来自多个线程的不安全访问会导致不可预知的结果。但是您也看到可以从不安全访问中提取一些保证(也就是说,如果每个线程都写入 1,那么最终值不能神奇地变成其他值)。考虑以下代码 sn -p:"
  • @UserNotDefined:绝对错误。如果每个线程都写入 1,并且存在导致未定义行为的竞争条件,则结果可以是任何东西。您的应用程序可能会崩溃。

标签: c multithreading concurrentmodification


【解决方案1】:

假设每个线程都有一个本地的i(即,每个线程无论如何都会运行 5 次迭代),让我们尝试得到 1 作为结果。这意味着 last 线程写入值必须在其 5th 迭代中为 n 读取 0。发生这种情况的唯一方法是,如果在该线程的第 5 次迭代开始时还没有线程写入 n,但要使该线程处于其第 5 次迭代,该线程本身必须已写入 n,因此它是不可能。

因此,最小可能的结果是 2,这可能会发生,例如,如下所示:最后一个写入 n 的线程已经完成 4 次迭代,然后另一个线程写入 1,最后一个线程在其开始时读取 1第 5 次迭代,所有其他线程在最后一个线程之前完成所有迭代,最后最后一个线程完成其第 5 次迭代,写入 2。

免责声明:我正在回答关于多线程的概念问题——正如其他人所指出的,如果 C 代码缺乏原子性可能会导致未定义的行为和任意结果提供的按原样使用。基于问题的“不言而喻”的最大数字案例,我猜教科书的作者要么没有意识到这一点,要么正在使用类似 C 的伪代码来说明这个概念。如果是前者,那么正确的答案是这本书是错误的,但我认为后一种情况的答案也是有教育意义的。

【讨论】:

  • 免责声明:我正在回答关于多线程的概念性问题——正如其他人所指出的,写入的非原子性可能会导致未定义的行为和任意结果。根据问题“不言而喻”的最大数字案例,我猜教科书的作者要么没有意识到这一点,要么正在使用类似 C 的伪代码来说明这个概念。
  • 作为附加说明,我假设i 是每个线程的本地是必要的,即使未定义的行为和原子性问题被忽略,教科书的答案也是正确的。如果i 被共享,那么一个线程只能运行一次迭代(其他线程增加了i 以使循环条件为假)并设置n = 1。因此,可以肯定地假设教科书的作者打算使用非共享i
  • 对否决票的解释?我觉得我已经解释了我所做的假设,如果有人因为我“回答练习”而投反对票,请注意 OP 已经从他们的教科书中得到了答案……
【解决方案2】:

添加一些见解:在 C 中使用 + 运算符进行加法、减法等不仅仅是 1 次操作。在汇编级别下,+ 操作由多条指令组成。如果多个线程要访问一个变量,并且这些指令的交错错误,最终结果可能是非常不正确的结果 -> 这就是我们需要互斥锁、信号量和条件变量之类的另一个原因。

【讨论】:

  • 这是一个 C 问题,不是 C++
  • @MattMcNabb 已更正,但对 C 来说仍然如此。
  • 添加是一个单一的操作,但加载和存储是分开的(即使显然编码在 CISC 上的单一指令中)
  • @o11c 我在 C 标准的什么地方可以找到它?还是您的意思是它恰好在某些平台上?
  • @DavidSchwartz 每个平台的装配级别都下降了
【解决方案3】:

最大的应该是不言而喻的:25,从 5 个线程开始增加 5 个。

完全和完全错误。不管怎么说,这不应该被听(至少关于线程的事情),句号。

 int tmp = n;
 tmp = tmp + 1;
 n = tmp;

想象一个 CPU 没有递增操作,但有一个高效的“加 10”操作和一个高效的“减 9”操作。在这样的 CPU 上,tmp = tmp + 1; 可以优化为tmp += 10; tmp -= 9;。编译器还可以通过对n 进行操作来完全优化tmp

所以这段代码可能相当于:

n += 10;
n -= 9;

现在想象会发生这种情况:所有五个线程加 10,所以n 现在是 50。第一个线程读取 50,其他四个线程减去 9。第一个线程从它读取的 50 中减去 9,然后写入 41。所以当一切都完成后,n 是 41。

因此,声称不言而喻的东西是完全错误的。谁写的都不懂 C 中的线程。

如果每个线程都写一个 1,那么最终值就不能神奇地变成别的东西

也是完全错误的。考虑一个通过先写入 0 然后递增值来写入 1 的 CPU。如果这发生在两个内核上,最终结果可能是 2。这本教科书是由根本不了解线程和未定义行为的人编写的。

(我假设这本教科书不限于某些特定的上下文,其中它所说的是真实的。例如,它可能使用“类 C”代码作为平台中立的汇编语言的一种形式,并且它可能正在对对齐整数具有特定保证的平台做出假设。但如果是这样,它所教的内容根本不会转换为 C 代码,并且仅适用于在其规则的 CPU 上编写汇编代码的人符合教科书的假设。)

【讨论】:

  • 即使经过编译器优化,线程不能在运行时仍然串行运行并以n=25结束,即(1+1+1+1+1)*5吗?
  • 对,但关键是最大值不是不言而喻。即使只有五个线程运行一次循环,n 也可能是 41!跨度>
  • 即使禁止任何编译器优化?
  • @UserNotDefined 是的,即使没有任何编译器优化,因为编译器可以做的任何优化都可以由 CPU 或系统的其他一些组件来完成。 (你要么有保证,要么没有,你不能通过排除你能想象到的会违反保证的事情来综合保证。)
  • @UserNotDefined 也因为编译器必须天真地将 C 代码转换为最接近的可想象的相应汇编代码的想法,或者它正在“优化”它只是错误的。可能有一个 CPU 根本没有增量操作,因此从较小的操作中进行增量是必要的,而不是优化。这是 C 代码,它意味着它的含义,需要发生的事情。
【解决方案4】:

关键是线程共享同一个数据实例。此外,似乎假设所有其他线程都以相同的执行速度运行。

因此,随着每个线程循环循环(到达fori++ 部分),它们几乎同时递增i,所以就好像代码是编写好的:

 for (i = 0; i < 5; i++, i++, i++, i++, i++)
    ...

至少在迭代次数最少的极端情况下。

【讨论】:

  • 但在迭代次数最少的情况下,对我来说,答案似乎是 5。你能解释一下为什么它会变成 2 吗?
  • @UserNotDefined:步骤是,i = 0(第一次迭代),然后是i=5。所以它实际上只能执行一次迭代。我觉得2的答案不正确。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-07-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-28
相关资源
最近更新 更多