什么问题可以在互斥范围外调用std :: list / vector / map / deque :: empty()?

时间:2015-12-08 09:36:46

标签: c++ multithreading concurrency containers mutex

请考虑以下代码段:

主题1:

while (true) {
    task = fetch_task();
    {
        lock_guard<mutex> lock(my_mutex);

        // modify content of my_list
        my_list.push_back(task);
    }
}

主题2:

while (true) {
    if (!my_list.empty()) {
        {
            lock_guard<mutex> lock(my_mutex);

            // modify content of my_list
            if (!my_list.empty()) {
                task = my_list.pop_front();
            }
        }
        if (taks) {
            handle_taks(task);
        }
    }
    do_some_other_stuff();
}

我知道这个代码在很多层面很差(比如异常处理)并且在任务处理方面还有更好的方法 - 我&m; m只对一个方面感兴趣:

我在互斥范围之外调用my_list.empty()(可能是为了避免在性能危急情况下锁定my_mutex)。

闻起来很糟糕,我不打算这样做 - 但我想知道真的会发生什么。empty()返回bool - 我能否至少假设这个电话是安全的吗?

当然,在我调用empty()后内容可能已经改变,所以我必须通过再次检查互斥范围内的empty()来避免竞争条件。

所以我的问题是 - 这种代码在现实世界中对误报误报甚至崩溃有什么影响?

2 个答案:

答案 0 :(得分:5)

对来自不同线程的标准库类成员的非同步访问,其中至少有一个线程正在写入导致数据争用(除非明确定义,但通常不是这种情况)。数据竞争是未定义的行为。未定义的行为可能导致任意结果。

换句话说:列表中的一切都可能发生。还有很多其他事情可以发生。

答案 1 :(得分:1)

我相信,知道可能的结果对于发挥作用是有用的 - 当你目击特定的行为时,你可以猜出原因。出于这个原因,我将尝试分析该场景。

让我们先看看筹码。首先,标准要求empty()的复杂度保持不变 - 这意味着它从单个变量读取(不迭代列表)。它可能是列表中的布尔成员。 (在C ++ 11之后,它最有可能以size()的形式实现,但是前C ++ 11 size()不需要是恒定的复杂性)。

现在,如果它是一个布尔成员,则必须在每次修改列表时更新它。由于变量读取周围没有互斥锁,因此这些更新有可能在读取线程中不可见。对bools的撕裂读取(这种情况不太可能发生,因为我知道没有可以做到这一点的架构)不会影响这一点,因为它既可以是也可以。因此,如果不是,您可能会将列表视为空。

现在,让我们来看看C ++级别的东西。这也很有趣。如果编译器在第一次迭代时可以证明list是空的,那么整个if语句将被优化掉。

结论: 可能的结果 - 读者没有任何反应,好像列表总是空的。