像for_each这样的并行算法与周围的代码同步吗?
这是在思考Thread sanitizer warnings after using parallel std::for_each时出现的。
具有并行执行策略的std::for_each
算法可以在实现创建的工作线程中执行代码。这些线程是否与调用线程对for_each
的调用和返回同步,或者类似的情况?常识似乎表明它们应该这样做,但我在C++20标准中找不到保证。
考虑以下简单示例(try on godbolt):
#include <algorithm>
#include <execution>
#include <iostream>
void increment(int &a) {
a++;
}
int main(void) {
constexpr size_t n = 1000;
static int arr[n];
arr[0] = 3;
std::for_each(std::execution::par, arr, arr+n, increment);
std::cout << arr[0] << std::endl;
return 0;
}
这将始终输出4
。
该实现可以在另一个线程中调用increment(arr[0])
,该线程执行arr[0]++
。主线程中的存储arr[0] = 3
是否发生在arr[0]++
之前?同样,arr[0]++
是否发生在std::cout << arr[0]
中arr[0]
的加载之前?我天真地认为他们应该这样做,但我看不到任何方法来证明这一点。算法。并行似乎不包含与周围代码同步的任何内容。
如果不是,则该示例包含数据竞争,并且其行为未定义。这将使std::execution::par
的正确使用变得相当困难,我想知道这是否是缺陷。
如果没有这样的保证,可以想象该实现可能会执行以下操作:
std::atomic<int *> work = nullptr;
void do_work() {
int *p;
while (!(p = work.load(std::memory_order_relaxed)))
std::this_thread::yield();
(*p)++;
}
// started at program startup
std::thread worker_thread(do_work);
int main() {
// ...
arr[0] = 3;
// for_each does the following:
work.store(&arr[0], std::memory_order_relaxed);
worker_thread.join();
// ...
}
如果是这样的话,我们真的会有一场数据竞赛。
解决方案
使用cppreference:
用作唯一类型的执行策略类型,用于消除并行算法重载的歧义,并指示并行算法的执行可以并行化。允许使用此策略(通常指定为std::Execution::PAR)调用并行算法中的元素访问函数,以在调用线程或由库隐式创建的线程中执行,以支持并行算法执行。在同一线程中执行的任何此类调用彼此之间的顺序不确定。
在std::for_each
中(逻辑上)创建的线程中完成的操作是在线程创建后排序的。
发件人the draft:
允许使用Execution?::?PARALLEL_POLICY类型的执行策略对象调用并行算法中的元素访问函数,以在调用执行的线程或由库隐式创建的执行线程中执行,以支持并行算法执行。 如果由线程([thread.thread.class])或j线程([thread.jthread.class])创建的执行线程提供并发向前进度保证([Intro.Progress]),则由库隐式创建的执行线程将提供并行向前进度保证;否则,所提供的向前进度保证是实现定义的。 在同一执行线程中执行的任何此类调用彼此之间的顺序不确定。
措辞略有不同,但相似。
我想您可以绕过它;没有明确的保证由库隐式创建的支持并行算法执行的线程需要在foreach
方法中创建(或联接)。
但需要满足各种算法的后置条件,它应该处理";After";问题;如何保证后置条件在std::for_each
返回之前发生,但可以保证后置条件已经发生。在我看来,应用程序发生在std::for_each
返回之前。
对于启动排序,我能做的最好的事情就是阅读标准,这意味着它必须在std::for_each
中为此目的而创建的线程行为,因此有排序保证。但我承认这个措辞有点含糊,由图书馆创作的是相当被动的语气。
相关文章