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

fri*_*tzo 8 c++ multithreading atomic vectorization c++11

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];
        }
    }
};
Run Code Online (Sandbox Code Playgroud)

注意operator | =非常接近我实际代码中的内容,但insert(std :: set)只是试图捕获一个人可以的想法

acquire lock;
make many atomic accesses in parallel;
release lock;
Run Code Online (Sandbox Code Playgroud)

我的问题是:混合这种原子访问和非原子访问的最佳方法是什么?下面[1,2]的答案表明铸造错误(我同意).但是,标准肯定允许这样明显安全的访问?

更一般地说,一个人可以使用读写器锁并允许"读者"以原子方式读写,并且独特的"作者"可以非原子地读写吗?

参考

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

Jir*_*ika 5

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

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

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

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

所以最好的实际答案是简单性.设计您的跨线程交互尽可能简单,而不会完全破坏可扩展性,响应性或任何其他神圣的牛; 几乎没有共享的可变数据结构; 并尽可能少地访问它们,总是原子地.

  • 最后一段+1.另一方面,非原子访问的跨线程行为,只要它们都是读取,就可以很好地定义; 只有当你输入一两个写入时才会得到未定义的行为.这是来自[intro.multithread]/21及其前身.C++ 11**之前的C++确实有一个内存模型,但它不支持多线程应用程序. (2认同)