std::mutex 用法示例

std::mutex usage example

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

#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 个线程 ab,我希望找到数据竞争。输出是 200000 而不是随机数。为什么?

此代码是使用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?

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

这里的问题是您的数据竞争非常小。任何现代编译器 will convert your inc function to counter += a,所以竞争 window 非常小 - 我什至会说,很可能一旦你启动第二个线程,第一个线程就已经完成了。

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

竞争条件是未定义的行为

当涉及数据竞争时,您无法断言应该发生什么。你断言 应该 有一些可见的数据撕裂证据(即最终结果是 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 的写入有可能(并且很可能)在第二个线程初始化之前就已经完成,这使得数据撕裂的观察在统计上是不可能的。

如果您想强制此代码按您期望的方式运行,请考虑将变量 counter 标记为 volatile:

#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 的循环也可能非常快。