为什么通过std :: lock_guard获取的互斥锁似乎在其作用域之后仍然有效了一段时间

时间:2019-06-08 11:08:58

标签: c++ mutex raii object-lifetime

我们知道,正确使用std :: lock_guard就像这种RAII样式:

void increase_decrease() {
    std::lock_guard<std::mutex> guard(global_mutex);

    static const int times = 50;
    for (int i = 0; i < times; i++) {
        global_data ++;
    }
    for (int i = 0; i < times; i++) {
        global_data --;
    }
}

在这里,我的重点是关于如何使用std::lock_guard或互斥锁。

在下面的代码中,我们故意错误的方式使用std::lock_guard。 (也就是说,将其放在关键部分之前的一个块中。)

创建16个线程,以将全局int变量加1并从中减去1,该变量被初始化为0,持续50次。

std::lock_guard在一个块中被调用,并且该块在关键部分之前(错误!切勿执行此类操作!)。遵循RAII-style机制,互斥锁将在阻止之后再次释放(用法错误)。因此,当它进入关键部分时,就没有锁了。

#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
#include <vector>

int global_data = 0;
std::mutex global_mutex;
void increase_decrease() {
    // XXX: INCORRECT USAGE! INCORRECT USAGE! INCORRECT USAGE!
    {
        std::lock_guard<std::mutex> guard(global_mutex);
    }
    // // XXX: uncomment to sleep for a litter while
    // std::this_thread::sleep_for(std::chrono::milliseconds(10));
    static const int times = 50;
    for (int i = 0; i < times; i++) {
        global_data ++;
    }
    for (int i = 0; i < times; i++) {
        global_data --;
    }
}

void try_mutex() {
    const int num_workers = 16;
    std::vector<std::thread> workers;

    auto start = std::chrono::system_clock::now();
    for (int i = 0; i < num_workers; i++) {
        std::thread t(increase_decrease);
        workers.push_back(std::move(t));
    }
    for (auto &t: workers) {
        t.join();
    }
    auto end = std::chrono::system_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "global_data: " << global_data
        << ", elapsed second: " << elapsed_seconds.count();
}

int main() {
    try_mutex();
}

我发现睡眠10毫秒是否会导致不同的结果。

不睡觉,主呼叫的20个标准输出为:

global_data: 0, elapsed second: 0.000363
global_data: 0, elapsed second: 0.000359
global_data: 0, elapsed second: 0.000349
global_data: 0, elapsed second: 0.000345
global_data: 0, elapsed second: 0.000352
global_data: 0, elapsed second: 0.000323
global_data: 0, elapsed second: 0.000619
global_data: 0, elapsed second: 0.000431
global_data: 34, elapsed second: 0.000405
global_data: -14, elapsed second: 0.000415
global_data: 0, elapsed second: 0.000497
global_data: 0, elapsed second: 0.000366
global_data: 0, elapsed second: 0.000413
global_data: 0, elapsed second: 0.000406
global_data: 0, elapsed second: 0.000353
global_data: 0, elapsed second: 0.000363
global_data: 0, elapsed second: 0.000361
global_data: 0, elapsed second: 0.000358
global_data: 0, elapsed second: 0.000348
global_data: 0, elapsed second: 0.000367

但是,如果我们取消睡眠注释,则main的20个调用的标准输出为:

global_data: 44, elapsed second: 0.011108
global_data: 15, elapsed second: 0.010645
global_data: 25, elapsed second: 0.012905
global_data: 27, elapsed second: 0.012914
global_data: 9, elapsed second: 0.012871
global_data: 46, elapsed second: 0.012836
global_data: 44, elapsed second: 0.011307
global_data: -2, elapsed second: 0.01286
global_data: 77, elapsed second: 0.012853
global_data: 43, elapsed second: 0.011984
global_data: 0, elapsed second: 0.011134
global_data: -3, elapsed second: 0.011571
global_data: 49, elapsed second: 0.012438
global_data: 43, elapsed second: 0.011552
global_data: -20, elapsed second: 0.010807
global_data: 0, elapsed second: 0.010514
global_data: 0, elapsed second: 0.010916
global_data: -44, elapsed second: 0.012829
global_data: 50, elapsed second: 0.011759
global_data: 9, elapsed second: 0.012873

第一种情况下global_data等于0的可能性比第二种情况更大。我尝试了很多次。 只是一个巧合。

因此,似乎互斥体有机会在通过std::lock_guard获得的区块之后生效一会儿。为什么?

谢谢。

2 个答案:

答案 0 :(得分:4)

  

std::lock_guard在关键部分之前的块中调用。遵循RAII风格的机制,互斥锁应在阻止之后释放。

否,互斥锁会在您放入的块的末尾释放。而且那是在访问global_data之前的。因此,它不受保护,这是未定义的行为。您可以通过未定义的行为看到所看到的结果,并且您不应该花费太多精力来理解这些结果。

如果您删除lock_guard周围的括号,它将很好用。

答案 1 :(得分:3)

您在变量global_data上进行了数据竞争(因为在开始操纵它之前先释放了它)。因此,运行代码的结果是不可预测的。

如果将global_dataint更改为std::atomic <int>,则无论有无sleep(全部为零),都将得到相同的输出。

Live demo