【问题标题】:Do parallel algorithms like for_each synchronize with surrounding code?像 for_each 这样的并行算法是否与周围的代码同步?
【发布时间】:2022-01-13 14:58:20
【问题描述】:

这是在考虑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]++。在intro.races p10 的意义上,主线程中的arr[0] = 3 是否发生在之前 arr[0]++?同样,arr[0]++ 发生在arr[0]std::cout &lt;&lt; arr[0] 中的负载之前吗?我天真地期望他们应该这样做,但我看不出有任何方法可以证明这一点。 algorithm.parallel 似乎没有包含与周围代码同步的任何内容。

如果不是,则该示例包含数据竞争并且其行为未定义。这会使正确使用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();
    // ...
}

如果是这样,那么我们真的会进行数据竞赛。

【问题讨论】:

  • 我认为this 可能是您正在寻找的保证
  • @NathanOliver:如果我们将“X 完成时的阻塞”解释为 X 的效果发生在解除阻塞之前,那可能会使用以下代码来处理比赛,并且在线程解除阻塞时可见。该标准实际上并没有在任何地方说,这是一个单独的问题,请参阅stackoverflow.com/questions/70228390/…

标签: c++ concurrency language-lawyer c++20


【解决方案1】:

使用cppreference

执行策略类型用作唯一类型以消除并行算法重载的歧义并指示并行算法的执行可以并行化。使用此策略调用的并行算法中元素访问函数的调用(通常指定为 std::execution::par)被允许在调用线程或由库隐式创建的线程中执行以支持并行算法执行。在同一线程中执行的任何此类调用相对于彼此的顺序是不确定的。

std::for_each 中创建的线程(逻辑上)中完成的操作是在线程创建之后排序的。

来自the draft

并行算法中元素访问函数的调用使用类型为 execution​::​parallel_policy 的执行策略对象被允许在调用执行线程或由库隐式创建以支持的执行线程中执行并行算法执行。 如果由 thread ([thread.thread.class]) 或 jthread ([thread.jthread.class]) 创建的执行线程提供并发前向进度保证 ([intro.progress]),则由图书馆将提供并行的前进进度保证;否则,提供的前进进度保证是实现定义的。 在同一个执行线程中执行的任何此类调用相对于彼此的顺序是不确定的。

措辞略有不同但相似。

我想你可以绕开它;没有明确保证需要在 foreach 方法中创建(或加入)库隐式创建的支持并行算法执行的线程。

但是需要满足各种算法的后置条件,应该处理“后”问题;没有指定std::for_each 返回的之前后条件如何保证发生,但可以保证后置条件已经发生。在我看来,应用程序发生在std::for_each 返回之前。

对于启动排序,我能做的最好的就是阅读标准,因为它必须表现得好像线程是为此目的在std::for_each 中创建的,所以有一个排序保证。但我承认这个措辞有点模糊,“created by the library”是相当被动的语态。

【讨论】:

  • 啊,对,后置条件。我想这是阻止for_each 触发所有线程并立即返回的唯一原因,这显然很愚蠢。所以它必须保证线程的工作实际上是“完成”的。如果它完成了,但还不能保证可见,那还有什么意义呢?所以这似乎合理地暗示了同步。
  • 至于启动,在我看来,标准确实希望允许实现使用预先创建的工作线程池,所以我认为它不想要求 @ 987654329@ 本身实际上启动或加入了它使用的线程。
  • @NateEldredge 您可以要求“好像”没有实际新线程的新线程。您只需要清除诸如 thread_local 存储之类的东西并保证发生在之前。一旦线程死亡,使用来自它的信息(如它的 ID)不能保证是合理的(或唯一的)。 (每次产生线程和重用池之间唯一可观察到的定义差异是thread_local 变量初始化和销毁​​)
猜你喜欢
  • 2013-04-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-24
  • 1970-01-01
  • 2011-02-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多