【问题标题】:data visibility between threads without lock没有锁的线程之间的数据可见性
【发布时间】:2017-04-03 22:05:13
【问题描述】:

我了解 C++11 中内存排序的基本规则,尤其是释放-获取排序。我在两个线程之间共享了一大块内存,我不需要原子性,但希望确保最终由一个线程完成的所有更改在另一个线程中都是可见的,尤其是在具有宽松内存模型的平台上。

可以简单地使用原子保护变量来触发内存同步吗?例如,

std::atomic<bool> guardVar;
char *shared_mem=get_shared_mem();

(thread 1)
while(true) {
  happens_many_things();
  do_whatever_I_want_with_shared_mem();
  guardVar.store(0, std::memory_order_release);
}

(in thread 2)
while(true) {
  guardVar.load(std::memory_order_acquire);
  read_shared_mem_no_problem_if_inconsistent();
}

同样,如果线程 2 在 do_whatever_I_want_with_shared_mem() 中间读取“半就绪”状态也不是问题,我只是想确保在明确定义的点之后获得线程 1 写入的所有更改。

基于this article 它应该可以工作,但我在网上没有看到这样的解决方案,并且不容易测试它是否真的符合我的意图。

没事吧?如果是,有没有更优雅的方式?

【问题讨论】:

    标签: multithreading c++11 synchronization shared-memory atomic


    【解决方案1】:

    如果线程 2 在 do_whatever_I_want_with_shared_mem() 中间读取到“半就绪”状态,这不是问题

    这是一个错误,如果其中一个线程正在修改数据,您将无法通过多个线程访问共享内存。 C++ 标准将其称为数据竞争,它会导致未定义的行为。

    两个线程之间的访问需要同步,但是你使用std::atomic的方式不对。线程 1 中的 store_release 紧随其后的是再次访问相同的数据。 load_acquire 相同;两个操作之间没有同步,因此您正在处理数据竞争。

    为了确保您的共享内存一次只能被一个线程访问,guardVar 在技术上可以这样使用:

    std::atomic<bool> guardVar{false};
    
    (thread 1)
    while(true) {
    
        while (guardVar.exchange(true, std::memory_order_acquire));  // LOCK
    
        happens_many_things();
        do_whatever_I_want_with_shared_mem();
    
        guardVar.store(false, std::memory_order_release);  // UNLOCK
    }
    
    (in thread 2)
    while(true) {
    
        while (guardVar.exchange(true, std::memory_order_acquire)); // LOCK
    
        read_shared_mem_no_problem_if_inconsistent();
    
        guardVar.store(false, std::memory_order_release);  // UNLOCK
    }
    

    但由于这是使用std::atomic 作为互斥体,效率相当低(注意旋转),你真的应该使用std::mutex

    更新:

    仍然可以在没有锁定的情况下使用您的共享内存,但是您有责任确保在共享内存中访问的每个单独的对象都是无数据竞争的(std::atomic 对象符合条件)。

    然后,您或多或少会得到您在问题中描述的行为,其中第二个线程可能会看到“半就绪”状态(某些对象已更新,其他对象未更新)。如果没有同步,第二个线程无法真正知道第一个线程的更新何时完成,但至少可以安全地同时读取/写入无数据竞争的对象。

    【讨论】:

    • 嗯。你说得对,这确实是一种未定义的行为。
    • 嗯。您是对的,这确实是标准未定义的行为。想象一下,我正在模拟一个帧缓冲区,一个线程在随机访问中连续写入的内容,而另一个线程想要定期读出整个缓冲区来处理它。撕裂不是问题。如何在不锁定任何线程的情况下解决这个问题?
    • @Ferenc 这种情况确实是你问题的一部分。我会更新答案
    • 在我之前评论中提到的帧缓冲区场景中,创建约 1M 原子对象(缓冲区中的每个字节一个)实际上并不可行。无论如何,我接受您的回答,因为您正确回答了我原来的问题。也许我应该重新措辞并针对这个具体的帧缓冲区案例提出一个新问题。
    • 为了警告追随者,我发现this article 关于数据竞赛。现在我看到了两种可能的解决方案:1)在汇编中编写读取线程,并在第一个循环中保留释放存储语义以强制执行内存围栏指令,或者 2)将帧缓冲区声明为易失性数组,从而防止编译器优化参考文章中描述的如此讨厌的事情。这两个选项都不符合标准,但我无法想象编译器如何欺骗我。 :)
    猜你喜欢
    • 1970-01-01
    • 2013-04-22
    • 2016-03-04
    • 1970-01-01
    • 2013-01-22
    • 1970-01-01
    • 2014-07-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多