【问题标题】:operator ++ (prefix) with threads带线程的运算符 ++(前缀)
【发布时间】:2016-06-01 12:32:07
【问题描述】:

朋友之间的赌注。 sum 变量被定义为全局变量。 我们有 2 个线程在循环 1..100 上运行,并且每个循环将 sum 加 1。

将打印什么? “总和=”?

int sum = 0;

void func(){
    for (int i=0 ; i<= 100; i++){
        sum++;
    }
}

int main(){

  t1 = Thread(func);
  t2 = Thread(func);

  t1.start();
  t2.start();

  t1.join();
  t2.join();

  cout << "sum = " << sum;

  return 0;

}

【问题讨论】:

  • 任何事情都可能发生。它是 UB - http://stackoverflow.com/questions/37325524/does-integer-overflow-cause-undefined-behavior-because-of-memory-corruption/37325854#37325854
  • 出于好奇,我更正了 c++11 的程序,第一个结果是“202”,这意味着任何认为范围是 100-200 的人都是错误的。出于一个可能的原因,请仔细查看代码;)但它仍然是 UB。
  • 注意标题是指prefix operator++,但代码使用postfix。并不是说这会影响结果...

标签: c++ multithreading operators


【解决方案1】:

这是未定义的行为,所以我要说 42。当您有多个线程访问共享变量并且其中至少有一个是写入器时,您需要同步。如果您没有该同步,那么您有未定义的行为,我们无法告诉您会发生什么。

您可以使用std::mutexstd::atomic 来获得同步并定义程序行为。

【讨论】:

  • 我知道我需要互斥锁来锁定关键代码。但是值范围是多少?
  • @ItayAveksis Theroreticlly 它可能是任何东西,但可能在 100-200 之间。问题是您可能在增量部分撕毁了读取,因此您不知道您要增加的实际值是什么。
  • 但还是 100
  • @ItayAveksis 不必如此。可能会,但谁真的能说。这是未定义的行为。
【解决方案2】:

sum 没有单一值。如果有 0 个竞态条件,则该值将为 200。如果循环的每次迭代(不太可能)都有竞态条件,则它可能低至 100。或者它可能介于两者之间。

您可能认为 sum++ 是一个原子操作,但它实际上是 sum = sum + 1 的语法糖。此操作中可能存在竞争条件,因此每次运行时 sum 都可能不同。

假设 sum 的当前值为 10。然后 t1 进入循环并读取 sum (10) 的值,然后停止让 t2 开始运行。然后 t2 将读取与 t1 相同的 sum 值 (10)。然后,当每个线程递增时,它们都会将其递增到 11。如果没有其他竞争条件,则 sum 的最终值为 199。

这是一个更糟糕的情况。想象一下 sum 的当前值又是 10。 t1 进入循环并读取 sum (10) 的值,然后停止以让 t2 开始运行。 t2 再次读取 sum (10) 的值,然后自身停止。现在 t1 再次开始运行,它循环了 10 次,将 sum 的值设置为 20。现在 t2 再次启动并将 sum 增加到 11,所以实际上你已经减少了 sum 的值。

【讨论】:

  • 展示价值如何减少的好例子。
  • 如果底层硬件写入不是原子的或变量未对齐,情况会更糟。由于所讨论的值范围有限,这不太可能,但想象一下,当一个线程尝试写入 0x0100,第二个 0x00FF 时,您将以第一个线程的最不重要和第二个线程的最重要结束。
  • 没有递减。你刚刚损失了 10 个增量。
  • @AhmadWabbi 增量仍然发生,所以你没有丢失它们。 “递减”的意思是减少价值,所以从 20 变为 10 是递减。
  • @Rick 增量发生了,但是你失去了它们的效果,因为旧值 10 发生了。该值相对递减,但总体而言,您丢失了一些 ++ 操作。我只是反对“递减”这个词,因为它具有误导性。最终结果能否为负数或小于 100?如果有递减,你如何解释答案“否”(如果是“否”)?返回旧值不等于“递减”
【解决方案3】:

由于自增不是atomic,所以会产生undefined behaviour

【讨论】:

  • 您链接到法国 cppreference。我什至不知道有法语 cppreference
  • @ItayAveksis:首先'未定义的行为'意味着你不应该做进一步的假设,其次我们不知道Thread是如何实现的,这也无济于事。
  • @stefaanv:你说得对!我错过了,因为页面顶部的“信息”是英文的。已更正。
  • @ItayAveksis :正如另一个答案中所解释的,可能在 100 到 200 之间,具体取决于错过的增量数量。但是,我不完全确定,它可能取决于特定的实现,从而导致非常奇怪的行为。
【解决方案4】:

这将是一个介于 100 和 200 之间的随机值。两个线程之间存在竞争条件,没有互斥。所以,一些++操作会丢失。这就是为什么当一个线程的所有 ++ 操作都丢失时你会得到 100,而当没有任何东西丢失时你会得到 200。两者之间的任何事情都可能发生。

【讨论】:

  • 没有。未定义的行为意味着未定义。结果不限制在 100 到 200 之间。(请注意,此程序的串行版本会生成 202)。
  • 它在 100 到 202 之间(不是 200,因为循环中有 i
  • 语言定义说这个程序的行为是undefined。这意味着语言定义并没有告诉你程序做了什么。 (如果要求结果在特定范围内,则结果将是未指定)您可以根据自己的喜好推测可能会发生什么,但除非您的编译器记录了它是如何发生的处理这个,你的猜测没有任何意义。没有什么可以阻止编译器观察到多个线程在没有同步的情况下写入同一位置,优化增量并将值设置为 42。
  • @PeteBecker 你的论点太有哲理了,我不会继续这样的讨论,因为它没有用。尽管它在语言定义中是未定义的,但实际上它是未指定的。编译器与它无关,是线程的本质,不可能是42。
猜你喜欢
  • 1970-01-01
  • 2011-03-12
  • 1970-01-01
  • 2016-03-24
  • 1970-01-01
  • 2022-11-24
  • 1970-01-01
相关资源
最近更新 更多