【问题标题】:std::mutex usage examplestd::mutex 使用示例
【发布时间】:2018-07-16 20:28:56
【问题描述】:

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

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

【问题讨论】:

  • 行为未定义,它可以做任何事情,包括给出你期望的结果。它可能不在另一个平台上、另一个平台上或以后。也许增量运算符在您的平台上是原子的?
  • 对于这个应用程序,你可以使用std::atomic&lt;int&gt; counter;。看来您的平台具有原子int 增量。 std::atomic&lt;int&gt; 可以便携地利用这一点。
  • @FrançoisAndrieux 我使用 VS2017 和英特尔 i7700k。我的平台没有原子增量操作!我用 Delphi 编写了相同的代码,非互斥体给了我随机值
  • 如果您坚持推测未定义的行为,您需要分享使用的确切编译器版本和编译标志。
  • 即使除了未定义的行为问题,即使在-O1,gcc 和 clang 都将您的函数实现为单个 add 指令......所以你不太可能看到这些冲突...... .

标签: c++ multithreading mutex data-race


【解决方案1】:

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

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

【讨论】:

  • 我什至没有想过这种优化...谢谢!
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    竞争条件是未定义的行为

    当涉及数据竞争时,您无法断言应该发生什么。您关于应该有一些明显的数据撕裂证据(即最终结果是 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 的循环也可能非常快。

    【讨论】:

    • 这就是我一直在寻找的答案!当我看到 C++ 正确地做到了这一点(非互斥方式),而使用另一种语言 Delphi,由于未定义的行为(仍然是非互斥方式),我能够重现随机输出数时,我的脑海中出现了麻烦。跨度>
    • 也可以使用 volatile 关键字,我不再得到确切的数字了!
    • 即使没有优化。创建线程的成本很高。通过循环运行的成本低。在创建第二个线程之前,第一个线程可以简单地完成所有迭代。
    • @MartinYork 因此我在最后一段中建议增加所涉及的数字。值得注意的是,人们倾向于高估线程创建的“成本”。这在很大程度上取决于 OS + 底层硬件,但即使我们假设增加一个变量是单个 CPU 周期(不太可能),一个线程仍然可以在 100000 个 CPU 周期内完全创建。
    • @Xirema:评论并不是真的针对你(只是为了让其他人提供更多背景信息)。我看你理解这个问题。我想说关于线程创建最昂贵的部分是新堆栈区域的分配。内存管理是这里的瓶颈(即使您使用操作系统而不是语言内存处理程序)。其余的似乎相对微不足道。
    猜你喜欢
    • 2017-03-28
    • 2013-12-29
    • 2012-11-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多