具有强大内存模型的平台上的易失性和多线程
volatile and multithreading on platform with strong memory models
几乎任何时候我们处理多线程时,volatile
都会被滥用(至少对于任何跨平台代码)。但是,我很好奇如果在 x86(-64) 等架构上使用 volatile bool
会给出(不确定的)但正确的结果,其中(单个)加载和存储在(正确对齐的)字上-sized 值是原子的(因此读取看不到中间值;它总是会看到 true
或 false
)。
例如,考虑如下多线程线性搜索:
#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>
。它解决了所有三个问题,并且将被编写以利用底层硬件,也许是以您没有想到的方式。
几乎任何时候我们处理多线程时,volatile
都会被滥用(至少对于任何跨平台代码)。但是,我很好奇如果在 x86(-64) 等架构上使用 volatile bool
会给出(不确定的)但正确的结果,其中(单个)加载和存储在(正确对齐的)字上-sized 值是原子的(因此读取看不到中间值;它总是会看到 true
或 false
)。
例如,考虑如下多线程线性搜索:
#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>
。它解决了所有三个问题,并且将被编写以利用底层硬件,也许是以您没有想到的方式。