std :: mutex用法示例

时间:2018-07-16 20:28:56

标签: c++ multithreading mutex data-race

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

#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?

3 个答案:

答案 0 :(得分:6)

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

这不会使未定义的行为变少,但是可以解释您看到的结果。您可能会降低编译器对循环的了解,例如通过制作akcounter volatile;那么您的数据竞争就会变得显而易见。

答案 1 :(得分:3)

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

答案 2 :(得分: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的写操作可能(并且很可能)在第二个线程甚至尚未初始化之前就完成了,从而使统计上的数据撕裂的观察变得不可能。

如果要强制此代码以预期的方式运行,请考虑将变量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也可以很快。