【问题标题】:What are the constraints on the user using STL's parallel algorithms?使用 STL 的并行算法对用户有哪些限制?
【发布时间】:2017-05-10 22:00:04
【问题描述】:

在杰克逊维尔会议上,提案P0024r2 有效地采用了Parallelism TS 的规范,被C++17 (draft) 接受。该提议为许多采用 执行策略 参数的算法添加了重载,以指示应考虑哪种并行性。 <execution>(20.19.2 [execution])中已经定义了三个执行策略:

  • std::execution::sequenced_policy (20.19.4 [execpol.seq]) 和 constexpr 对象 std::execution::seq (20.19.7 [parallel.execpol.objects]) 表示顺序执行,类似于在没有执行策略的情况下调用算法。李>
  • std::execution::parallel_policy (20.19.5 [execpol.par]) 和 constexpr 对象 std::execution::par (20.19.7 [parallel.execpol.objects]) 表示可能使用多个线程执行算法。
  • std::execution::parallel_unsequenced_policy (20.19.6 [execpol.vec]) 和 constexpr 对象 std::execution::par_unseq (20.19.7 [parallel.execpol.objects]) 表示可能使用向量执行和/或多线程执行算法。

STL 算法通常将用户定义的对象(迭代器、函数对象)作为参数。 对用户定义的对象有哪些限制,才能使它们与使用标准执行策略的并行算法一起使用?

例如,当使用如下示例中的算法时,FwdItPredicate 的含义是什么?

template <typename FwdIt, typename Predicate>
FwdIt call_remove_if(FwdIt begin, FwdIt end, Predicate predicate) {
    return std::remove_if(std::execution::par, begin, end, predicate);
}

【问题讨论】:

标签: c++ stl c++17


【解决方案1】:

简短的回答是,使用执行策略std::execution::parallel 的算法使用的元素访问函数(本质上是算法对各种参数所需的操作;详情见下文)是不允许的导致数据竞争或死锁。与使用执行策略std::execution::parallel_unsequenced_policy 的算法一起使用的元素访问函数也不能使用任何阻塞同步。

细节

描述基于选票文件 N4604。我还没有核实某些条款是否针对国家机构 cmet 进行了修改(粗略的检查似乎暗示到目前为止没有任何修改)。

第 25.2 节 [algorithms.parallel] 指定并行算法的语义。有多个约束不适用于不采用执行策略的算法,分为多个部分:

  1. 在 25.2.2 [algorithms.parallel.user] 中限制谓词函数可以对其参数执行的操作:

    作为PredicateBinaryPredicateCompareBinaryOperation 类型的对象传递给并行算法的函数对象不得通过其参数直接或间接修改对象。

    该子句的编写方式似乎可以修改对象本身,只要遵守其他约束(见下文)。请注意,此约束与执行策略无关,因此即使在使用 std::execution::sequenced_policy 时也适用。完整的答案比这更复杂,目前看来规范无意中受到了过度约束(请参阅下面的最后一段)。

  2. 在 25.2.3 [algorithms.parallel.exec] 中添加了针对不同执行策略的元素访问函数(见下文)的约束:

    • 使用std::execution::sequenced_policy 时,元素访问函数都从同一个线程调用,即,执行不会以任何形式交错。
    • 当使用std::execution::parallel_policy 时,不同的线程可能会同时从不同的线程调用元素访问函数。不允许从不同线程调用元素访问函数导致数据竞争或死锁。但是,来自同一线程的元素访问调用是[不确定的]顺序,即,没有来自同一线程的元素访问函数的交错调用。例如,如果与std::execution::par 一起使用的Predicate 计算它被调用的频率,则需要适当地同步相应的计数。
    • 当使用std::execution::parallel_unsequenced_policy 时,元素访问函数的调用可以在不同线程之间以及在一个执行线程内交错。也就是说,使用阻塞同步原语(如std::mutex)可能会导致死锁,因为同一个线程可能会尝试多次同步(例如,尝试多次锁定同一个互斥锁)。将标准库函数用于元素访问函数时,标准中的约束是(25.2.3 [algorithms.parallel.exec] 第 4 段):

      如果一个标准库函数被指定为与另一个函数调用同步,或者另一个函数调用被指定为与之同步,并且它不是内存分配或释放函数,则它是向量化不安全的。从execution::parallel_unsequenced_policy算法调用的用户代码不能调用向量化不安全的标准库函数。

    • 当使用实现定义的执行策略时会发生什么,不出所料,实现定义。

  3. 在 25.2.4 [algorithm.parallel.exception] 中,从元素访问函数抛出的异常的使用受到某种限制:当元素访问函数抛出异常时,std::terminate() 被调用。也就是说,抛出异常是合法的,但结果不太可能是可取的。请注意,即使使用std::execution::sequenced_policy,也会调用std::terminate()

元素访问函数

上述约束使用术语元素访问函数。该术语在 25.2.1 [algorithm.parallel.defns] 第 2 段中定义。有四组函数被归类为元素访问函数:

  • 用于实例化算法的迭代器类别的所有操作。
  • 对其规范要求的那些序列元素的操作。
  • 如果规范要求,用户提供的函数对象将在算法执行期间应用。
  • 规范要求的函数对象上的操作。

本质上,元素访问函数是标准在算法规范或与这些算法一起使用的概念中明确提及的所有操作。未提及的函数,例如检测到存在的函数(例如,使用 SFINAE)不受约束,并且实际上不能从对其使用施加同步约束的并行算法中调用。

问题

似乎无法保证 [mutating] 元素访问函数应用于的对象在不同线程之间是不同的,这有点令人担忧。特别是,我无法保证应用于迭代器对象的迭代器操作不能应用于来自两个不同线程的同一个迭代器对象!这意味着,例如,迭代器对象上的operator++() 需要以某种方式同步其状态。如果对象在不同的​​线程中被修改,我看不出 operator==() 如何做一些有用的事情。对同一对象的操作需要同步似乎是无意的,因为将 [mutating] 元素访问函数同时应用于对象没有任何意义。但是,我看不到任何说明使用了不同对象的文字(我想,我需要为此提出一个缺陷)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-06-16
    • 1970-01-01
    • 2013-07-03
    • 2013-10-20
    • 2010-12-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多