并行容器和对象

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

并发容器提供对最重要操作的并发安全访问。 在这里,并发安全意味着指针或迭代器始终有效。 它不保证元素初始化或特定的遍历顺序。 这些容器的功能与 C++ 标准库提供的功能类似。 例如,concurrency::concurrent_vector 类与 std::vector 类类似,但 concurrent_vector 类允许并行追加元素。 如果并行代码需要对同一容器进行读写访问,请使用并发容器。

并发对象在组件之间并发共享。 并行计算并发对象状态的进程生成的结果与连续计算相同状态的另一个进程相同。 concurrency::combinable 类是并发对象类型的一个示例。 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 类。 例如,对于名为 vconcurrent_vector 类型的变量,表达式 &v[0]+2 生成未定义的行为。

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

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

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

并发安全操作

追加到 concurrent_vector 对象或增加其大小或者访问 concurrent_vector 对象中的元素的所有方法都是并发安全方法。 在这里,并发安全意味着指针或迭代器始终有效。 它不保证元素初始化或特定的遍历顺序。 resize 方法是此项规则的例外情况。

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

运行时提供的用于实现与 C++ 标准库兼容的操作(例如 reserve)不是并发安全的。 下表显示了非并发安全的常见方法和运算符。

修改现有元素值的操作不是并发安全的。 使用同步对象(如 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 = begin(v); i != end(v); ++i) 
         {
            sums.local() += *i;
         }     
         wcout << L"sum = " << sums.combine(plus<int>()) << endl;
      }
   );
}

尽管 end 方法是并发安全的,但对 push_back 方法的并发调用会导致 end 返回的值发生更改。 迭代器遍历的元素数目不确定。 因此,每次运行该程序时,该程序会生成不同的结果。 如果元素类型不常用,则 push_backend 调用之间可能存在争用条件。 end 方法可能返回已分配但未完全初始化的元素。

异常安全

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

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

  • 析构函数不得引发。

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

[返回页首]

concurrent_queue 类

concurrency::concurrent_queue 类与 std::queue 类一样,它允许访问其前后的元素。 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 方法是并发安全的,但并发操作可能会导致队列在 empty 方法返回之前扩大或减小。

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

迭代器支持

concurrent_queue 提供了非并发安全的迭代器。 建议将这些迭代器仅用于调试。

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

运算符 说明
operator++ 前进到队列中的下一个项。 重载此运算符以提供前增量和后增量语义。
operator* 检索对当前项的引用。
operator-> 检索指向当前项的指针。

[返回页首]

concurrent_unordered_map 类

concurrency::concurrent_unordered_map 类是一个关联容器类,就像 std::unordered_map 类一样,它控制 std::pair<const Key, Ty> 类型的元素的可变长度序列。 将无序映射视为字典,你可以向其中添加键值对或按键查找值。 如果有多个线程或任务必须同时访问、插入到或更新共享容器,此类非常有用。

以下示例显示了用于使用 concurrent_unordered_map 的基本结构。 此示例将字符键插入范围 ['a', 'i']。 由于操作顺序不确定,因此每个键的最终值也不确定。 但是,可以安全地并行执行插入。

// unordered-map-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_unordered_map.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain() 
{
    //
    // Insert a number of items into the map in parallel.

    concurrent_unordered_map<char, int> map; 

    parallel_for(0, 1000, [&map](int i) {
        char key = 'a' + (i%9); // Geneate a key in the range [a,i].
        int value = i;          // Set the value to i.
        map.insert(make_pair(key, value));
    });

    // Print the elements in the map.
    for_each(begin(map), end(map), [](const pair<char, int>& pr) {
        wcout << L"[" << pr.first << L", " << pr.second << L"] ";
    });
}
/* Sample output:
    [e, 751] [i, 755] [a, 756] [c, 758] [g, 753] [f, 752] [b, 757] [d, 750] [h, 754]
*/

有关使用 concurrent_unordered_map 并行执行映射和化简运算的示例,请参阅如何:并行执行映射和化简运算

concurrent_unordered_map 和 unordered_map 之间的差异

concurrent_unordered_map 类与 unordered_map 类非常相似。 以下几点说明了 concurrent_unordered_mapunordered_map 的不同之处:

  • erasebucketbucket_countbucket_size 方法分别命名为 unsafe_eraseunsafe_bucketunsafe_bucket_countunsafe_bucket_sizeunsafe_ 命名约定表明这些方法不是并发安全的。 有关并发安全的详细信息,请参阅并发安全操作

  • 插入操作不会使现有指针或迭代器无效,也不会更改映射中已存在的项的顺序。 可以同时进行插入和遍历操作。

  • concurrent_unordered_map 仅支持正向迭代。

  • 插入不会使 equal_range 返回的迭代器无效或对其进行更新。 插入可以将不相等的项追加到范围末尾。 begin 迭代器指向相等的项。

为了帮助避免死锁,当 concurrent_unordered_map 调用内存分配器、哈希函数或其他用户定义的代码时,其方法不会持有锁。 此外,必须确保哈希函数始终将相等的键计算为相同的值。 最佳哈希函数在哈希代码空间中均匀地分布键。

并发安全操作

concurrent_unordered_map 类支持并发安全插入和元素访问操作。 插入操作不会使现有指针或迭代器无效。 迭代器访问和遍历操作也是并发安全的。 在这里,并发安全意味着指针或迭代器始终有效。 它不保证元素初始化或特定的遍历顺序。 下表显示了并发安全的常用 concurrent_unordered_map 方法和运算符。

虽然可以从并发运行的线程安全地调用 count 方法,但是如果同时将新值插入到容器中,不同的线程可能会收到不同的结果。

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

除了这些方法之外,任何以 unsafe_ 开头的方法也不是并发安全的。

[返回页首]

concurrent_unordered_multimap 类

concurrency::concurrent_unordered_multimap 类与 concurrent_unordered_map 类非常相似,只不过它允许多个值映射到同一个键。 它与 concurrent_unordered_map 的不同之处还包括以下方面:

以下示例显示了用于使用 concurrent_unordered_multimap 的基本结构。 此示例将字符键插入范围 ['a', 'i']。 concurrent_unordered_multimap 使键能够具有多个值。

// unordered-multimap-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_unordered_map.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain() 
{
    //
    // Insert a number of items into the map in parallel.

    concurrent_unordered_multimap<char, int> map; 

    parallel_for(0, 10, [&map](int i) {
        char key = 'a' + (i%9); // Geneate a key in the range [a,i].
        int value = i;          // Set the value to i.
        map.insert(make_pair(key, value));
    });

    // Print the elements in the map.
    for_each(begin(map), end(map), [](const pair<char, int>& pr) {
        wcout << L"[" << pr.first << L", " << pr.second << L"] ";
    });
}
/* Sample output:
    [e, 4] [i, 8] [a, 9] [a, 0] [c, 2] [g, 6] [f, 5] [b, 1] [d, 3] [h, 7]
*/

[返回页首]

concurrent_unordered_set 类

concurrency::concurrent_unordered_set 类与 concurrent_unordered_map 类非常相似,只不过它管理的是值而不是键值对。 concurrent_unordered_set 类不提供 operator[]at 方法。

以下示例显示了用于使用 concurrent_unordered_set 的基本结构。 此示例将字符值插入范围 ['a', 'i']。 可以安全地并行执行插入。

// unordered-set-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_unordered_set.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain() 
{
    //
    // Insert a number of items into the set in parallel.

    concurrent_unordered_set<char> set; 

    parallel_for(0, 10000, [&set](int i) {
        set.insert('a' + (i%9)); // Geneate a value in the range [a,i].
    });

    // Print the elements in the set.
    for_each(begin(set), end(set), [](char c) {
        wcout << L"[" << c << L"] ";
    });
}
/* Sample output:
    [e] [i] [a] [c] [g] [f] [b] [d] [h]
*/

[返回页首]

concurrent_unordered_multiset 类

concurrency::concurrent_unordered_multiset 类与 concurrent_unordered_set 类非常相似,只不过它允许重复值。 它与 concurrent_unordered_set 的不同之处还包括以下方面:

以下示例显示了用于使用 concurrent_unordered_multiset 的基本结构。 此示例将字符值插入范围 ['a', 'i']。 concurrent_unordered_multiset 使值能够出现多次。

// unordered-set-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_unordered_set.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain() 
{
    //
    // Insert a number of items into the set in parallel.

    concurrent_unordered_multiset<char> set; 

    parallel_for(0, 40, [&set](int i) {
        set.insert('a' + (i%9)); // Geneate a value in the range [a,i].
    });

    // Print the elements in the set.
    for_each(begin(set), end(set), [](char c) {
        wcout << L"[" << c << L"] ";
    });
}
/* Sample output:
    [e] [e] [e] [e] [i] [i] [i] [i] [a] [a] [a] [a] [a] [c] [c] [c] [c] [c] [g] [g]
    [g] [g] [f] [f] [f] [f] [b] [b] [b] [b] [b] [d] [d] [d] [d] [d] [h] [h] [h] [h]
*/

[返回页首]

combinable 类

concurrency::combinable 类提供了可重用的线程本地存储,该存储允许你执行细化的计算,然后将这些计算合并为最终的结果。 你可以将 combinable 对象当作 reduction 变量。

如果拥有在多个线程或任务之间共享的资源,combinable 类非常有用。 combinable 类通过以无锁方式提供对共享资源的访问权限来帮助消除共享状态。 因此,该类提供了使用同步机制(例如互斥体)同步从多个线程对共享数据进行访问的替代方法。

方法和功能

下表显示了 combinable 类的一些重要方法。 有关所有 combinable 类方法的详细信息,请参阅 combinable 类

方法 说明
local 检索对与当前线程上下文关联的局部变量的引用。
clear combinable 对象中删除所有线程局部变量。
combine

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

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

调用 combinecombine_each 方法后,可以在对象 combinable 中存储其他数据。 还可以多次调用 combinecombine_each 方法。 如果 combinable 对象中的本地值没有发生更改,则每次调用 combinecombine_each 方法时,这些方法都会生成相同的结果。

示例

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

[返回页首]

如何:使用并行容器提高效率
显示如何使用并行容器高效地并行存储和访问数据。

如何:使用 combinable 提高性能
显示如何使用 combinable 类来消除共享状态,从而提高性能。

如何:使用 combinable 来组合集
显示如何使用 combine 函数合并线程本地数据集。

并行模式库 (PPL)
介绍 PPL,它提供命令式编程模型,可以促进开发并发应用程序的可扩展性和易用性。

参考

concurrent_vector 类

concurrent_queue 类

concurrent_unordered_map 类

concurrent_unordered_multimap 类

concurrent_unordered_set 类

concurrent_unordered_multiset 类

combinable 类