如何在C++中混合原子操作和非原子操作

How to mix atomic and non-atomic operations in C++?

本文关键字:原子操作 混合 C++      更新时间:2023-10-16

std::atomic类型允许对变量进行原子访问,但有时我会像非原子访问一样,例如当访问受到互斥锁的保护时。考虑一个允许多线程访问(通过插入)的位字段类和单线程矢量化访问(通过运算符|=):

class Bitfield
{
    const size_t size_, word_count_;
    std::atomic<size_t> * words_;
    std::mutex mutex_;
public:
    Bitfield (size_t size) :
        size_(size),
        word_count_((size + 8 * sizeof(size_t) - 1) / (8 * sizeof(size_t)))
    {
        // make sure words are 32-byte aligned
        posix_memalign(&words_, 32, word_count_ * sizeof(size_t));
        for (int i = 0; i < word_count_; ++i) {
            new(words_ + i) std::atomic<size_t>(0);
        }
    }
    ~Bitfield () { free(words_); }
private:
    void insert_one (size_t pos)
    {
        size_t mask = size_t(1) << (pos % (8 * sizeof(size_t)));
        std::atomic<size_t> * word = words_ + pos / (8 * sizeof(size_t));
        word->fetch_or(mask, std::memory_order_relaxed);
    }
public:
    void insert (const std::set<size_t> & items)
    {
        std::lock_guard<std::mutex> lock(mutex_);
        // do some sort of muti-threaded insert, with TBB or #pragma omp
        parallel_foreach(items.begin(), items.end(), insert_one);
    }
    void operator |= (const Bitfield & other)
    {
        assert(other.size_ == size_);
        std::unique_lock<std::mutex> lock1(mutex_, defer_lock);
        std::unique_lock<std::mutex> lock2(other.mutex_, defer_lock);
        std::lock(lock1, lock2); // edited to lock other_.mutex_ as well
        // allow gcc to autovectorize (256 bits at once with AVX)
        static_assert(sizeof(size_t) == sizeof(std::atomic<size_t>), "fail");
        size_t * __restrict__ words = reinterpret_cast<size_t *>(words_);
        const size_t * __restrict__ other_words
            = reinterpret_cast<const size_t *>(other.words_);
        for (size_t i = 0, end = word_count_; i < end; ++i) {
            words[i] |= other_words[i];
        }
    }
};

注意运算符|=与我的实际代码非常接近,但insert(std::set)只是试图捕捉一个想法,一个人可以

acquire lock;
make many atomic accesses in parallel;
release lock;

我的问题是:混合这种原子和非原子的最佳方法是什么通道对下面[1,2]的回答表明选角是错误的(我同意)。但这个标准肯定允许这样明显安全的访问吗?

更一般地,可以使用读写器锁并允许"读写"吗;读者";原子地读和写;作家;非原子地读写?

参考文献

  1. 如何有效地使用std::atomic
  2. 访问原子<int>作为非原子的C++0x

C++11之前的标准C++没有多线程内存模型。我认为定义非原子访问的内存模型的标准没有变化,因此这些标准得到了与C++11之前的环境类似的保证。

实际上,理论上它甚至比使用memory_order_relaxed更糟糕,因为非原子访问的跨线程行为只是完全未定义的,而不是多种可能的执行顺序,其中一种最终必须发生。

因此,要在混合原子访问和非原子访问的同时实现这些模式,您仍然必须依赖于特定于平台的非标准结构(例如,_ReadBarrier)和/或对特定硬件的深入了解。

一个更好的选择是熟悉memory_order枚举,并希望通过给定的代码和编译器实现最佳的汇编输出。最终结果可能是正确的、可移植的,并且不包含不需要的内存栅栏,但如果你像我一样,你应该首先分解和分析几个有缺陷的版本;并且仍然不能保证在所有代码路径上使用原子访问不会在不同的体系结构或不同的编译器上导致一些多余的围栏。

因此,最好的实用答案是简单第一。尽可能简单地设计跨线程交互,而不会完全扼杀可扩展性、响应性或任何其他神圣的奶牛;几乎没有共享的可变数据结构;并尽可能少地访问它们,始终是原子式的。

如果你能做到这一点,你可能会有(潜在的)一个线程使用原子访问读取/写入数据对象,另一个线程在不使用原子访问的情况下读取/写入同一数据对象。这是一场数据竞赛,行为将是未定义的。

在C++20中有std::atomic_ref,它允许对非原子数据进行原子操作。

因此,您应该能够将words_声明为非原子size_t*,并在需要时使用std::atomic_ref<size_t>执行原子操作。但要注意以下要求:

当存在引用对象的任何atomic_ref实例时对象必须通过这些atomic_ref进行独占访问实例。atomic_ref引用的对象没有子对象对象可以由任何其他atomic_ref对象同时引用。

upd:在这种特殊情况下,您可能还需要std::shared_mutex来分离原子";读者的";从非原子";作家的";修改。