请考虑以下代码段:
主题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()
来避免竞争条件。
所以我的问题是 - 这种代码在现实世界中对误报,误报甚至崩溃有什么影响?
答案 0 :(得分:5)
对来自不同线程的标准库类成员的非同步访问,其中至少有一个线程正在写入导致数据争用(除非明确定义,但通常不是这种情况)。数据竞争是未定义的行为。未定义的行为可能导致任意结果。
换句话说:列表中的一切都可能发生。还有很多其他事情可以发生。
答案 1 :(得分:1)
我相信,知道可能的结果对于发挥作用是有用的 - 当你目击特定的行为时,你可以猜出原因。出于这个原因,我将尝试分析该场景。
让我们先看看筹码。首先,标准要求empty()
的复杂度保持不变 - 这意味着它从单个变量读取(不迭代列表)。它可能是列表中的布尔成员。 (在C ++ 11之后,它最有可能以size()的形式实现,但是前C ++ 11 size()不需要是恒定的复杂性)。
现在,如果它是一个布尔成员,则必须在每次修改列表时更新它。由于变量读取周围没有互斥锁,因此这些更新有可能在读取线程中不可见。对bools的撕裂读取(这种情况不太可能发生,因为我知道没有可以做到这一点的架构)不会影响这一点,因为它既可以是也可以。因此,如果不是,您可能会将列表视为空。
现在,让我们来看看C ++级别的东西。这也很有趣。如果编译器在第一次迭代时可以证明list是空的,那么整个if语句将被优化掉。
结论: 可能的结果 - 读者没有任何反应,好像列表总是空的。