【问题标题】:How to make the shared value behave consistently without mutex?如何在没有互斥锁的情况下使共享值的行为一致?
【发布时间】:2021-07-01 23:04:35
【问题描述】:

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

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 &gt; 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 吗?

【问题讨论】:

  • get_counter() 返回的值应该是连续的,但不能保证它们会按顺序写入shared。您可以直接增加shared,这将给出顺序输出
  • 有没有办法“直接增加shared”而不将互斥锁放在主函数中,这是我想要避免的?get_counter 中的互斥锁只是为了演示。
  • 只是shared++?
  • 您是否必须处理从get_counter 获得的每一个值?例如,如果 writer1 从 get_counter 获得 10,而 writer2 获得 8,但尝试在 writer1 之后写入,您是否仍想将 shared 设置为 8 还是可以放弃对 get_counter 的调用并重试(有效地跳过 8 你刚得到)?
  • 您知道sharedif 条件下的值和发送到cout 的值可能不同吗?

标签: c++ multithreading atomic


【解决方案1】:

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

 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 触发。

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

【讨论】:

  • 小错字:Writer2: shared = 2 应该是Writer1: shared = 2
  • 好的。我明白为什么它现在失败了。但是你如何建议在不将互斥锁引入主函数的情况下修复它?如何使获取get_counter() 值并存储到shared 一个原子操作?我认为这是std::atomic 应该保证的。
  • @CrendKing 为什么你还有 get_counter()?为什么不直接触发shared++fetch_add(1)
  • @ALX23z 在这里引用 OP:get_counter 只是生成严格递增数字的助手。实际上它可以被其他更有用的功能取代
  • @CrendKing 的 atmoic 部分只是保证所持有的值始终处于有效状态(即没有线程可以同时写入存储的对象)。对get_counter 的函数调用不是此保证的一部分。尝试内联函数(即在赋值之前复制粘贴整个函数体)并考虑它的含义,如果atomic 必须保证所有这些新代码在最后的赋值方面都是 atmoic。跨度>
【解决方案2】:

从概念上讲,时间在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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-12-28
    • 1970-01-01
    • 2015-09-17
    • 2014-12-18
    • 1970-01-01
    • 1970-01-01
    • 2021-10-10
    相关资源
    最近更新 更多