0

我已经编写了这段代码作为测试:

#include <iostream>
#include <thread>
#include <mutex>

int counter = 0;

auto inc(int a) {
    for (int k = 0; k < a; ++k)     
        ++counter;
}

int main() {

    auto a = std::thread{ inc, 100000 };
    auto b = std::thread{ inc, 100000 };

    a.join();
    b.join();

    std::cout << counter;
    return 0;
}

counter变量是全局变量,因此创建了 2 个线程a,并且b我希望找到数据竞争。输出是 200000 而不是随机数。为什么?

此代码是使用 a 的固定版本,mutex因此全局变量只能访问一次(每次 1 个线程)。结果仍然是 200000 。

std::mutex mutex;

auto inc(int a) {
    mutex.lock();
    for (int k = 0; k < a; ++k)     
        ++counter;
    mutex.unlock(); 
}

事实是这样的。互斥体解决方案给了我 200000 这是正确的,因为一次只有 1 个威胁可以访问计数器。但是为什么非互斥解决方案仍然显示200000?

4

3 回答 3

6

这里的问题是您的数据竞争非常小。任何现代编译器都会将你的inc函数转换为counter += a,所以比赛窗口非常小——我什至会说,很可能一旦你启动第二个线程,第一个线程就已经完成了。

这并没有减少这种未定义的行为,而是解释了您所看到的结果。您可能会使编译器对您的循环不那么聪明,例如通过制作aorkcounter volatile; 那么你的数据竞争应该会变得很明显。

于 2018-07-16T20:37:26.617 回答
3

数据竞争是未定义的行为,这意味着任何程序执行都是有效的,包括恰好执行您想要的操作的程序执行。在这种情况下,编译器可能正在优化您的循环counter += a,并且第一个线程在第二个线程启动之前完成,因此它们实际上不会发生冲突。

于 2018-07-16T20:32:34.173 回答
2

竞争条件是未定义的行为

当涉及数据竞争时,您无法断言应该发生什么。您断言应该有一些明显的数据撕裂证据(即最终结果是 178592 或其他东西)是错误的,因为没有理由期待任何这样的结果。

您观察到的行为可能可以通过编译器优化来解释

以下代码

auto inc(int a) {
    for (int k = 0; k < a; ++k)     
        ++counter;
}

可以根据 C++ 标准合法优化成

auto inc(int a) {
    counter += a;
}

请注意写入次数是如何counter从 优化O(a)到 的O(1)。这是相当重要的。这意味着有可能(并且很可能)写入counter在第二个线程甚至还没有被初始化之前就完成了,这使得观察数据撕裂在统计上是不可能的。

如果您想强制此代码以您期望的方式运行,请考虑将变量标记countervolatile

#include <iostream>
#include <thread>
#include <mutex>

volatile int counter = 0;

auto inc(int a) {
    for (int k = 0; k < a; ++k)     
        ++counter;
}

int main() {

    auto a = std::thread{ inc, 100000 };
    auto b = std::thread{ inc, 100000 };

    a.join();
    b.join();

    std::cout << counter;
    return 0;
}

请记住,这仍然是未定义的行为,不应在任何类型的生产目标代码中依赖!但是,此代码更有可能复制您尝试调用的竞争条件。

您也可以尝试大于 100000 的数字,因为在现代硬件上,即使没有优化,100000 的循环也可以非常快。

于 2018-07-16T20:40:35.927 回答