并行容器和对象

并行模式库 (PPL) 包含一些容器和对象,这些容器和对象提供对其元素的线程安全访问。

并发容器提供对最重要的操作的并发安全访问。 这些容器的功能类似于标准模板库 (STL) 所提供的功能。 例如,Concurrency::concurrent_vector 类与 std::vector 类相似,但 concurrent_vector 类允许您并行追加元素。 如果具有需要对相同容器进行读写访问的并行代码,请使用并发容器。

并发对象同时由组件共享。 某个并行计算并发对象状态的进程所产生的结果与按顺序计算相同状态的另一个进程所产生的结果相同。 Concurrency::combinable 类是并发对象类型的一个示例。 combinable 类允许您先并行执行计算,然后将这些计算结果合并成最终结果。 如果要另外使用某个同步机制(例如互斥体)来同步对共享变量或资源的访问,请使用并发对象。

各节内容

本主题详细描述下列并行容器和对象。

并发容器:

  • concurrent_vector 类

  • concurrent_queue 类

并发对象:

  • combinable 类

concurrent_vector 类

Concurrency::concurrent_vector 类是序列容器类,与 std::vector 类一样,它也允许您随机访问其元素。 concurrent_vector 类可实现并发安全追加和元素访问操作。 追加操作不会使现有指针或迭代器失效。 迭代器访问和遍历操作也是并发安全操作。

concurrent_vector 和 vector 之间的区别

concurrent_vector 类与 vector 类非常相似。 对 concurrent_vector 对象执行的追加操作、元素访问操作以及迭代器访问操作的复杂性与 vector 对象相同。 以下几点阐释了 concurrent_vectorvector 之间的差异:

  • concurrent_vector 对象的追加、元素访问、迭代器访问和迭代器遍历操作是并发安全操作。

  • 您只能将元素添加到 concurrent_vector 对象的末尾。 concurrent_vector 类不提供 insert 方法。

  • 追加到 concurrent_vector 对象时,该对象不使用移动语义

  • concurrent_vector 类不提供 erasepop_back 方法。 与 vector 一样,可使用 clear 方法从 concurrent_vector 对象中移除所有元素。

  • concurrent_vector 类未将其元素连续存储在内存中。 因此,不能按照数组的任何用法来使用 concurrent_vector 类。 例如,对于 concurrent_vector 类型的名为 v 的变量,表达式 &v[0]+2 将产生不确定的行为。

  • concurrent_vector 类可定义 grow_bygrow_to_at_least 方法。 这些方法类似于 resize 方法,只不过它们是并发安全方法。

  • 当您追加到 concurrent_vector 对象或调整其大小时,该对象不会重新定位其元素。 这将使现有指针和迭代器在执行并发操作期间保持有效。

  • 运行时未定义 bool 类型的 concurrent_vector 的专用版本。

并发安全操作

所有追加到 concurrent_vector 对象、增加其大小或访问 concurrent_vector 对象中的元素的方法都是并发安全方法。 对于此规则,resize 方法是一个例外。

下表显示了常见的并发安全的 concurrent_vector 方法和运算符。

at

end

operator[]

begin

front

push_back

back

grow_by

rbegin

capacity

grow_to_at_least

rend

empty

max_size

size

运行时为保持 STL 兼容性而提供的操作(例如 reserve)不是并发安全操作。 下表显示了常见的不是并发安全的方法和运算符。

assign

reserve

clear

resize

operator=

shrink_to_fit

修改现有元素的值的操作不是并发安全操作。 使用同步对象(如 reader_writer_lock 对象)将并发读写操作与相同的数据元素同步。 有关同步对象的更多信息,请参见同步数据结构

如果转换通过 vector 来使用 concurrent_vector 的现有代码,则并发操作可能会导致应用程序的行为发生更改。 例如,请考虑以下在 concurrent_vector 对象上并发执行两项任务的程序。 第一项任务将其他元素追加到 concurrent_vector 对象中。 第二项任务计算同一对象中所有元素的总和。

// parallel-vector-sum.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_vector.h>
#include <iostream>

using namespace Concurrency;
using namespace std;

int wmain()
{
   // Create a concurrent_vector object that contains a few
   // initial elements.
   concurrent_vector<int> v;
   v.push_back(2);
   v.push_back(3);
   v.push_back(4);

   // Perform two tasks in parallel.
   // The first task appends additional elements to the concurrent_vector object.
   // The second task computes the sum of all elements in the same object.

   parallel_invoke(
      [&v] { 
         for(int i = 0; i < 10000; ++i)
         {
            v.push_back(i);
         }
      },
      [&v] {
         combinable<int> sums;
         for(auto i = v.begin(); i != v.end(); ++i) 
         {
            sums.local() += *i;
         }     
         wcout << L"sum = " << sums.combine(plus<int>()) << endl;
      }
   );
}

尽管 end 方法是并发安全方法,但并发调用 push_back 方法将导致 end 返回的值发生更改。 迭代器遍历的元素数是不确定的。 因此,此程序在您每次运行它时都会产生不同的结果。

异常安全性

如果增长或赋值操作引发异常,则 concurrent_vector 对象的状态将变为无效。 除非另外声明,否则处于无效状态的 concurrent_vector 对象的行为不确定。 但是,析构函数总是释放对象分配的内存,即使该对象处于无效状态也是如此。

向量元素的数据类型 _Ty 必须满足以下要求。 否则,concurrent_vector 类的行为不确定。

  • 析构函数不得进行引发。

  • 如果默认构造函数或复制构造函数进行了引发,则不得使用 virtual 关键字声明析构函数,并且该析构函数必须能够用初始化为零的内存正常工作。

[转到页首]

concurrent_queue 类

std::queue 类一样,Concurrency::concurrent_queue 类也允许您访问其 front 和 back 元素。 concurrent_queue 类可实现并发安全排队和取消排队操作。 concurrent_queue 类还提供了不是并发安全的迭代器支持。

concurrent_queue 和 queue 之间的区别

concurrent_queue 类与 queue 类非常相似。 以下几点阐释了 concurrent_queuequeue 之间的差异:

  • concurrent_queue 对象执行的排队和取消排队操作是并发安全操作。

  • concurrent_queue 类提供了不是并发安全的迭代器支持。

  • concurrent_queue 类不提供 frontpop 方法。 concurrent_queue 类可通过定义 try_pop 方法替换这些方法。

  • concurrent_queue 类不提供 back 方法。 因此,您无法引用队列的末尾。

  • concurrent_queue 类提供了 unsafe_size 方法,而不是 size 方法。 unsafe_size 方法不是并发安全方法。

并发安全操作

所有对 concurrent_queue 对象进行排队或取消排队的方法都是并发安全方法。

下表显示了常见的并发安全的 concurrent_queue 方法和运算符。

empty

push

get_allocator

try_pop

尽管 empty 方法是并发安全方法,但是在 empty 方法返回之前,并发操作可能会导致队列增长或缩短。

下表显示了常见的不是并发安全的方法和运算符。

clear

unsafe_end

unsafe_begin

unsafe_size

迭代器支持

concurrent_queue 提供了不是并发安全的迭代器。 我们建议您只为调试而使用这些迭代器。

concurrent_queue 迭代器只向前遍历元素。 下表显示了每个迭代器支持的运算符。

运算符

说明

operator++

前进到队列中的下一项。 重载此运算符以提供前递增和后递增语义。

operator*

检索对当前项的引用。

operator->

检索指向当前项的指针。

[转到页首]

combinable 类

Concurrency::combinable 类提供了可重用的线程本地存储,以允许您执行精细计算,然后将这些计算合并为最终结果。 可以将 combinable 对象视为缩减变量。

当您有在几个线程或任务之间共享的资源时,combinable 类很有用。 combinable 类通过以无锁方式提供对共享资源的访问,可以帮助您消除共享的状态。 因此,此类为使用同步机制(例如互斥体)同步多线程中共享数据的访问提供了一种替代方法。

方法和功能

下表显示了 combinable 类的某些重要方法。 有关所有 combinable 类方法的更多信息,请参见combinable 类

方法

说明

local

检索对与当前线程上下文相关联的局部变量的引用。

clear

combinable 对象中移除所有线程本地变量。

combine

combine_each

使用提供的 combine 函数从所有线程本地计算的集合中生成最终值。

combinable 类是根据最终合并结果进行参数化的模板类。 如果调用默认构造函数,则 _Ty 模板参数类型必须具有默认构造函数和复制构造函数。 如果 _Ty 模板参数类型没有默认构造函数,则调用以初始化函数作为其参数的构造函数的重载版本。

在调用 combinecombine_each 方法之后,您可以在 combinable 对象中存储附加数据。 您还可以多次调用 combinecombine_each 方法。 如果 combinable 对象中的本地值没有发生更改,则每次调用 combinecombine_each 方法时,它们都会产生相同的结果。

示例

有关如何使用 combinable 类的示例,请参见以下主题:

[转到页首]

相关主题

参考

concurrent_vector 类

concurrent_queue 类

combinable 类