使用STL并行算法对用户有哪些限制?

What are the constraints on the user using STL's parallel algorithms?

本文关键字:STL 并行算法 用户 使用      更新时间:2023-10-16

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

  • 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.vc])与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);
}

简单的答案是,与使用执行策略std::execution::parallel的算法一起使用的元素访问函数(本质上是算法对各种参数所需的操作;有关详细信息,请参阅下文)不允许导致数据争用或死锁。与使用执行策略std::execution::parallel_unsequenced_policy的算法一起使用的元素访问函数另外不能使用任何阻塞同步。

详细信息

该描述基于投票文件N4604。我还没有核实其中一些条款是否是根据国家机构的评论而修改的(粗略检查似乎意味着到目前为止还没有编辑)。

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

  1. 在25.2.2〔algorithms.parallel.user〕中,限制了谓词函数对其参数的作用:

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

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

  2. 在25.2.3[算法.并行.执行]中,添加了对元素访问函数的约束(见下文),这些约束特定于不同的执行策略:

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

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

    • 毫不奇怪,当使用实现定义的执行策略时,会发生什么是实现定义的。

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

元素访问函数

上面的约束使用术语元素访问函数。该术语定义见25.2.1[算法.并行.定义]第2段。有四组功能被分类为元素访问功能:

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

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

问题

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