【问题标题】:Multithreading and sequence of instructions多线程和指令序列
【发布时间】:2022-01-24 19:03:09
【问题描述】:

在学习多线程编程时,我编写了以下代码。

#include <thread>
#include <iostream>
#include <cassert>

void check() {
    int a = 0;
    int b = 0;
    {
        std::jthread t2([&](){
            int i = 0;
            while (a >= b) {
                ++i;
            }
            std::cout << "failed at iteration " << i << "\n"
                      // I know at this point a and b may have changed
                      << a << " >= " << b << "\n";
            std::exit(0);
        });
        std::jthread t1([&](){
            while (true) {
                ++a;
                ++b;
            }
        });
    }
}

int main() {
    check();
}

因为++a 总是发生在++b 之前,所以a 应该总是大于或等于b。 但实验表明,有时b &gt; a。为什么?是什么原因造成的?我该如何执行?

即使我将 int a = 0; 替换为 int a = 1000;,这也让这一切变得更加疯狂。

程序很快退出,因此不会发生 int 溢出。 我没有发现任何指令在汇编中重新排序可能会导致这种情况。

【问题讨论】:

  • 上面的代码没有同步,所以存在竞争条件,我们有未定义的行为。修复 UB 后,我们可以尝试对线程进行推理。在此处查看线程和数据竞争 en.cppreference.com/w/cpp/language/memory_model "...如果发生数据竞争,则程序的行为未定义..."
  • 您假设t2 线程同时读取两个变量。 t2 可以将a 读入寄存器然后取消调度,t1 可以在再次调度t2 之前执行循环的100 次迭代,然后将b 读入寄存器,从而使b 更大。

标签: c++ multithreading


【解决方案1】:

在这些不同变量的增量和您读取它们时之间存在非平凡的竞争条件。如果您想要这些读取和写入的严格排序,您将不得不使用某种同步机制。 std::atomic 使它更容易。

试试这个:

#include<iostream>
#include <thread>
#include <iostream>
#include <cassert>
#include <atomic>

void check() {
    struct AB { int a = 0; int b=0; };
    std::atomic<AB> ab;
    {
        std::jthread t2([&](){
            int i = 0;
            AB temp;
            while (true) {
                temp = ab;
                if ( temp.a > temp.b ) break;
                ++i;
            }
            std::cout << "failed at iteration " << i << "\n"
                      // I know at this point a and b may have changed
                      << temp.a << " >= " << temp.b << "\n";
            std::exit(0);
        });
        std::jthread t1([&](){
            while (true) {
                AB temp = ab;
                temp.a++;
                temp.b++;
                ab = temp;
            }
        });
    }
}

int main() {
    check();
}

代码:https://godbolt.org/z/Kxeb8d8or

结果:

Program returned: 143
Program stderr
Killed - processing time exceeded

【讨论】:

  • 更简单——只需使用std::atomic&lt;int&gt;
  • std::atomic 只保证对该 int 的访问是原子的,不能同时访问
  • 它还保证它们之间的顺序一致性(memory_order_seq_cst),除非您使用一致性较低的显式操作。所以如果你在一个线程中有a++; b++;,在a的增量之前没有其他线程可以看到b的增量。您确实需要在您的测试循环中使用while (tmp = b, a &gt;= tmp) 以确保读取 b 在读取 a 之前,但这样应该很好。
  • 有些情况你没有考虑。想象一下: (1) 初始 a=0 b=0 (2) Thread1 - 读取 "a" a=0 (3) Thread2 - 递增 "a" a=1 (4) Thread2 - 递增 "b" b=1 ( 5) Thread1 - read "b" b=1 然后你会遇到 thread1 看到 a=0 和 b=1 的情况,虽然它们仍然是原子的,但它们是乱序的。
  • @ChrisDodd 手头案例:godbolt.org/z/vcennM8f6
【解决方案2】:

因为 ++a 总是在 ++b 之前发生,所以 a 应该总是更大或 等于 b

仅在其执行线程中。并且只有在执行线程可观察时。

C++ 需要一定的显式“同步”,以便一个执行线程所做的更改对其他执行线程可见。

     ++a;
     ++b;

仅使用这些语句,该执行线程无法真正“区分”a 还是 b 是否首先递增。因此,C++ 允许编译器实现它想要的任何优化或代码重新排序步骤,只要它在其执行线程中没有可观察到的效果,并且如果实际生成的代码递增b 首先不会有任何可观察到的效果。这个执行线程不可能分辨出来。

但是,如果有一些中间语句“查看”a,那么这将不再适用,编译器需要在以某种方式使用它之前实际生成递增 a 的代码.

这只是这个执行线程,单独的。即使可以在此执行线程中观察到ab 的相对更改顺序,根据标准,C++ 编译器也允许以任何顺序实际递增实际变量,只要还有任何其他使这种情况无法观察到的调整。但它可以被另一个执行线程观察到。为了防止这种情况发生,有必要采取明确的同步步骤,使用互斥锁、条件变量和 C++ 执行线程模型的其他部分。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多