如何在没有互斥的情况下使共享值的行为一致

How to make the shared value behave consistently without mutex?

本文关键字:共享 情况下      更新时间:2024-05-24

我有以下代码。我不明白为什么读者会看到不一致的变量值。

uint64_t get_counter() {
static uint64_t counter = 0;
static std::mutex m;
std::unique_lock l(m);
return ++counter;
}
auto main() -> int {
// uint64_t shared = 0;
std::atomic<uint64_t> shared = 0;
const auto writer = [&shared]() -> void {
while (true) {
shared = get_counter();
std::this_thread::yield();
}
};
const auto reader = [&shared]() -> void {
while (true) {
const uint64_t local = shared;
if (local > shared) {
cout << local << " " << shared << endl;
}
std::this_thread::yield();
}
};
std::thread w1(writer), w2(writer), r(reader);
r.join();
return EXIT_SUCCESS;
}

get_counter只是生成严格递增数的助手。事实上,它可以被其他更有用的功能所取代。

由于shared永远不应该变小,我希望在评估if (local > shared)时,它永远不应该是真的。然而,我得到的输出是这样的:

1022 1960
642677 644151
645309 645699
1510591 1512122
1592957 1593959
7964226 7965790
8918667 8919962
9094127 9095161
9116800 9117780
9214842 9215720
9539737 9541144
9737821 9739100
10222726 10223912
11197862 11199348

看起来local确实比shared小,但为什么输出呢?这是由某些数据竞赛引起的吗?如果是这样,如何在不引入互斥的情况下解决这个问题?std::atomic_thread_fence可以用来提供帮助吗?shared必须是std::atomic吗?

在我看来,以下序列是可能的:

Writer1: get_counter() -> 2
Writer2: get_counter() -> 3
Writer2: shared = 3
Reader: local = shared (3)
Writer1: shared = 2
Reader: if (local > shared) // 3 > 2

由于你的锁不包括生成+赋值,所以你有一个";竞赛条件";在写入到共享时,它可能从一代起就出现故障,导致if被解雇。

我把竞争条件放在引号里,因为它不是数据竞争(因为共享是原子的(。

从概念上讲,从get_counter()返回(解锁其锁(到返回值存储在shared之间经过一段时间。在该时间间隔内,另一个线程可以完成对get_counter()的调用,并将该(较大的(返回值存储在shared中,可能多次。然后,第一个线程存储它最初从get_counter()获得的值,该值较小。

例如:

Writer 1                  Writer 2                 Reader
Call get_counter()
get_counter() returns 5
Call get_counter()
get_counter returns 6
store 6 in shared
load 6 from shared
store 5 in shared
load 5 from shared

shared的存储实际上应该受到与counter相同的锁的保护,或者采取一些类似的措施来确保在可以存储计数器值之前,shared不会被任何其他线程写入。

栅栏在这里没用。它们处理的情况是,一个线程中的加载和存储对另一个线程可见,其顺序不是第一个线程的程序顺序。在这种情况下,即使每个操作都按照它在源中出现的顺序进行,也会有一场竞赛。

相关文章: