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

How to make the shared value behave consistently without mutex?

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

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() returns(解锁其锁)和返回值存储在 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 相同的锁的保护,或者采取一些类似的措施来确保 sharedshared 之前不被任何其他线程写入可以存储计数器值。

栅栏在这里无能为力。它们处理这样一种情况,即一个线程中的加载和存储以不是第一个线程的程序顺序的顺序对另一个线程可见。在这种情况下,即使每个操作都完全按照它在您的源代码中出现的顺序进行,您也有一场比赛。