【问题标题】:Does an atomic acquire synchronize with mutex lock release?原子获取是否与互斥锁释放同步?
【发布时间】:2023-12-27 00:22:02
【问题描述】:

我有一个对象,它在 unordered_map 中存储了一些设置,其中包含字符串键和变量值。由于我的库可能会被多个线程使用,并且读取的数量很可能会大大超过写入的数量,因此我考虑了一个写入时复制实现,其中“get”操作是无锁的,而“put”操作是关键的部分,如示例中所示:

class Cfg {
    using M = unordered_map<string,X>;
    shared_ptr<const M> data;
    mutex write_lock;
public:
    X get(string key) {
        shared_ptr<const M> cur_ver = atomic_load_explicit(&data, memory_order_acquire);
        // Extract the value from the immutable *cur_ver
    }
    void put(string key, X value) {
        lock<muted> wlock(write_lock);
        // No need for the atomic load here because of the lock
        shared_ptr<const M> cur_ver = data;
        shared_ptr<const M> new_ver = ;// create new map with value included
        // QUESTION: do I need this store to be atomic? Is it even enough?
        atomic_store_explicit(&data, new_ver, memory_order_release);
    }
}

只要获取/释放同步也会影响指向的数据而不仅仅是指针值,我有理由相信该设计有效。但是,我的问题如下:

  • 此操作是否需要锁内的原子存储
  • 或者原子获取是否会与互斥解锁同步,这是一个“释放”操作?

【问题讨论】:

    标签: c++ synchronization mutex atomic happens-before


    【解决方案1】:

    原子获取是否会与作为“释放”操作的互斥解锁同步?

    不,为了使获取操作与释放操作同步,获取操作必须观察释放操作的更改(或以该操作为首的潜在释放序列的某些更改)操作)。

    所以是的,您需要锁内的原子存储。无法保证get 将“看到”来自put 的最新值,因为您只使用获取/释放,因此存储和加载操作之间没有总顺序。如果你想要这个保证,你必须使用memory_order_seq_cst

    附带说明 - 这个实现很可能不是无锁的,因为在大多数库实现中,atomic_load_explicit for shared_ptr 不是无锁的。问题是您必须加载指针并取消引用该指针以增加引用计数器,在一个原子操作中。这在大多数架构上是不可能的,因此atomic_load_explicit 通常使用锁来实现。

    【讨论】:

    • 所以,如果我理解正确的话,没有atomic_store 它将不起作用,即使有它,由于缺少seq_cst,读者可能会看到旧版本...我对后者很好,因为写时复制实现意味着读者将看到 some 一致的版本。但是,关于 shared_ptr 中缺少原子无锁操作,是否也只使用锁进行读取会更好?
    • 是的,没有atomic_store 它不起作用(正确)。我不认为通过使操作 seq-cst 获得太多收益,我只是为了完整性而提到它。获取shared_ptr 的关键部分非常短,因此我希望它比在锁定下执行所有地图操作时扩展得更好,但您应该自己测试一下。或者,您可以使用 RW-Lock 或并发 hashmap(例如我的 xenium 库提供了一些)。
    【解决方案2】:

    如果您希望您的 get 函数始终返回最新值,则需要它。您可能会在同一时钟时间内发生多次读取和写入。使用原子内存顺序确保写入在读取之前的顺序。

    如果您混合使用非原子存储和原子加载,这是未定义的行为。这个thread 也讨论过。您可能有一个又一个的写入。如果你使用非原子指令,你可能会有数据竞争。

    根据cppreference

    memory_order_acquire

    一旦释放线程中所有对内存的访问(对加载线程有可见的副作用)发生后,该操作就会发生。

    memory_order_release

    该操作被安排在消费或获取操作之前发生,作为其他内存访问的同步点,可能对加载线程产生明显的副作用。

    【讨论】:

    • 我并不特别需要其他线程来保证每次都阅读最新版本,但我想避免 UB。从您的回答中,我了解到互斥锁解锁操作不会在原子负载之前建立发生,所以如果我将最后一行设为非原子,那么我必须保留它。