具有强大内存模型的平台上的易失性和多线程

volatile and multithreading on platform with strong memory models

几乎任何时候我们处理多线程时,volatile 都会被滥用(至少对于任何跨平台代码)。但是,我很好奇如果在 x86(-64) 等架构上使用 volatile bool 会给出(不确定的)但正确的结果,其中(单个)加载和存储在(正确对齐的)字上-sized 值是原子的(因此读取看不到中间值;它总是会看到 truefalse)。

例如,考虑如下多线程线性搜索:

#include <algorithm>
#include <future>
#include <thread>
#include <vector>

namespace detail
{

template <typename Iterator>
std::vector<std::pair<Iterator, Iterator>> split_range(
    Iterator begin, Iterator end, unsigned nranges, std::random_access_iterator_tag
)
{
    std::vector<std::pair<Iterator, Iterator>> split(nranges);
    auto diff = end - begin;
    for(auto i = 0U; i < nranges - 1; ++i) {
        split[i] = std::make_pair(begin, begin + (diff / nranges));
        begin += (diff / nranges);
    }
    split[nranges - 1] = std::make_pair(begin, end);
    return split;
}

template <typename Iterator> 
Iterator async_find_impl(
    std::pair<Iterator, Iterator> range,
    typename std::iterator_traits<Iterator>::value_type value,
    volatile bool* finished
)
{
    auto begin = range.first;
    auto end = range.second;

    while(!(*finished) && (begin != end)) {
        if(*begin == value) {
            *finished = true;
            return begin;
        }
        ++begin;
    }
    return end;
}

}

template <typename Iterator>
std::vector<std::pair<Iterator, Iterator>> split_range(
    Iterator begin, Iterator end, unsigned ranges
)
{
    return detail::split_range(
        begin, end, ranges, 
        typename std::iterator_traits<Iterator>::iterator_category()
    );
}


template <typename Iterator>
Iterator async_find(
    Iterator begin, Iterator end, 
    typename std::iterator_traits<Iterator>::value_type value
)
{
    volatile bool found = false;
    const static auto default_launch = 4U;
    unsigned to_launch = std::thread::hardware_concurrency();
    if(to_launch == 0) { to_launch = default_launch; }

    std::vector<std::future<Iterator>> futures;
    auto ranges = split_range(begin, end, to_launch);

    for(auto&& range : ranges) {
        auto func = [&]() { return detail::async_find_impl(range, value, &found); };
        futures.emplace_back(std::async(func));
    }

    std::vector<Iterator> results;
    for(auto&& future : futures) {
        results.emplace_back(future.get());
    }

    for(auto i = 0U; i < results.size(); ++i) {
        if(results[i] != ranges[i].second) {
            return results[i];
        }
    }

    return end;
}

显然,正确的 跨平台 方法是使用 std::atomic<bool>。但是,考虑到上面列出的假设,这会是 "correct" (正确的是返回任何指向给定值的 Iterator ;如果有多种可能性,本质上它是不确定的返回)?

C 和 C++ 中的原子解决了 三个 问题:撕裂、重新排序和可见性。当线程切换发生在需要多个总线周期的读取或写入中间时(例如,在 32 位处理器上 运行 时写入 64 位值),以及新的-运行 线程写入或读取(分别)该值。重新排序可以由编译器 由处理器完成。出现可见性问题是因为每个处理器都有自己的本地数据缓存;在将新值写入主内存然后读入另一个处理器的本地缓存之前,一个处理器的写入对另一个处理器不可见。

一个bool值可能存储在一块足够小的内存中,不会被撕裂,尽管这不是必需的。 volatile 告诉编译器不要重新排序读取和写入(也不要删除它们)。这些都没有解决可见性问题。所以使用 volatile bool 可能 会给你想要的结果,尽管不能保证什么时候可以看到更新的值。但是为什么要打扰呢?不要手动滚动你自己的一半措施,只需使用 atomic<bool>。它解决了所有三个问题,并且将被编写以利用底层硬件,也许是以您没有想到的方式。