Share via


平行容器和物件

平行模式程式庫 (PPL) 包含數個容器和物件,這些容器和物件提供對其項目的執行緒安全存取。

「並行容器」(Concurrent Container) 提供對最重要作業的並行安全存取。 這些容器的功能與標準範本程式庫 (STL) 所提供的容器類似。 例如,concurrency::concurrent_vector 類別與 std::vector 類別類似,不同之處在於 concurrent_vector 類別可讓您平行附加項目。 當您的平行程式碼需要同時對相同容器進行讀取和寫入存取時,請使用並行容器。

「並行物件」(Concurrent Object) 是各元件並行共用的物件。 以平行方式計算並行物件的狀態,會產生與以循序方式計算該並行物件的狀態相同的結果。 concurrency::combinable 類別是並行物件型別的一個範例。 combinable 類別可讓您平行執行計算,然後再將這些計算結合成最終的結果。 並行物件可以做為替代方案,來替代使用同步處理機制 (例如 Mutex) 同步處理對共用變數或資源的存取。

章節

本主題詳述下列平行容器和物件。

並行容器:

  • concurrent_vector 類別

    • concurrent_vector 與 vector 之間的差異

    • 並行安全的作業

    • 例外狀況安全

  • concurrent_queue 類別

    • concurrent_queue 與 queue 之間的差異

    • 並行安全的作業

    • Iterator 支援

  • concurrent_unordered_map 類別

    • 在 concurrent_unordered_map 和 unordered_map 之間的差異

    • 並行安全的作業

  • concurrent_unordered_multimap 類別

  • concurrent_unordered_set 類別

  • concurrent_unordered_multiset 類別

並行物件:

  • combinable 類別

    • 方法和功能

    • 範例

concurrent_vector 類別

concurrency::concurrent_vector 類別是序列容器類別,就像 std::vector 類別一樣,它可讓您隨機存取其項目。 concurrent_vector 類別啟用並行安全的附加和項目存取作業。 附加作業並不會讓現有指標或 Iterator 失效。 Iterator 存取和周遊作業也是並行安全的。

concurrent_vector 與 vector 之間的差異

concurrent_vector 類別與 vector 類別類似。 concurrent_vector 物件上附加、項目存取和 Iterator 存取作業的複雜度與 vector 物件是相同的。 下列各點說明 concurrent_vectorvector 的差異:

  • concurrent_vector 物件上的附加、項目存取、Iterator 存取和 Iterator 周遊作業是並行安全的。

  • 您只能將項目加至 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 物件中附加項目或變更物件的大小時,這個物件並不會更動其內項目的位置。 這可讓現有指標和 Iterator 在並行作業期間一直保持有效。

  • 執行階段不會針對 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 = begin(v); i != end(v); ++i) 
         {
            sums.local() += *i;
         }     
         wcout << L"sum = " << sums.combine(plus<int>()) << endl;
      }
   );
}

雖然 end 方法是並行安全的,但是並行呼叫 push_back 方法會變更 end 所傳回的值。 Iterator 會周遊的項目數是不一定的。 因此,每次執行時這個程式都會產生不同的結果。

例外狀況安全

如果成長或指派作業擲回例外狀況,則 concurrent_vector 物件的狀態會變無效。 除非另有陳述,否則處於無效狀態之 concurrent_vector 物件的行為會是未定義的行為。 不過,解構函式一定會釋出物件所配置的記憶體,即使物件處於無效狀態也是一樣。

vector 項目的資料型別 _Ty 必須符合下列需求。 否則,concurrent_vector 類別的行為會是未定義的行為。

  • 解構函式不得擲回例外狀況。

  • 如果預設或複製建構函式擲回例外狀況,則不得以 virtual 關鍵字宣告解構函式,而且解構函式必須能夠與以零初始化的記憶體正常互動。

[上方]

concurrent_queue 類別

concurrency::concurrent_queue 類別就像 std::queue 類別一樣,可讓您存取其 front 和 back 項目。 concurrent_queue 類別啟用並行安全的加入佇列和清除佇列作業。 concurrent_queue 類別也提供不是並行安全的 Iterator 支援。

concurrent_queue 與 queue 之間的差異

concurrent_queue 類別與 queue 類別類似。 下列各點說明 concurrent_queuequeue 的差異:

  • concurrent_queue 物件上的加入佇列和清除佇列作業是並行安全的。

  • concurrent_queue 類別則提供不是並行安全的 Iterator 支援。

  • 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

Iterator 支援

concurrent_queue 提供不是並行安全的 Iterator。 建議您只將這些 Iterator 用來進行偵錯。

concurrent_queue Iterator 只會以前進方向周遊項目。 下表顯示每個 Iterator 所支援的運算子。

運算子

說明

operator++

前進至佇列中的下一個項目。 這個運算子使用多載,來提供遞增前和遞增後的語意。

operator*

擷取目前項目的參考。

operator->

擷取目前項目的指標。

[上方]

concurrent_unordered_map 類別

concurrency::concurrent_unordered_map 類別是,就像 std::unordered_map 類別一樣,控制項型別 std::pair<常數, Ty 索引鍵>的項目不同長度序列的關聯的容器類別。 識別為未排序的 Map 做為字典將一對索引鍵與值加入至或索引鍵的值。 這個類別會很有用,就必須同時存取共用容器,插入它或更新它的多執行緒或工作時。

下列範例會示範 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_ 命名慣例表示這些方法不是並行安全的。 如需並行安全的詳細資訊,請參閱 並行安全操作作業。

  • 插入作業無法使用現有指標或 Iterator,也不會變更已存在於對應項目的順序。 插入和往返作業可能同時發生。

  • concurrent_unordered_map 只支援向前反覆項目。

  • 插入不會無效化或更新 equal_range所傳回的 Iterator。 外掛程式可附加不相等的項目加入至範圍的結尾。 開始的迭代器指向相等的項目。

為了避免死結, 當呼叫記憶體配置器、雜湊函式或其他任何使用者定義的程式碼,沒有任何 concurrent_unordered_map 的方法會保持鎖定。 此外,您也必須確定雜湊函式一律會評估為索引鍵為相同的值。 最佳雜湊函式跨雜湊碼空間統一散發索引鍵。

並行安全的作業

concurrent_unordered_map 類別啟用並行安全的插入和項目存取作業。 插入作業並不會讓現有指標或 Iterator 失效。 Iterator 存取和周遊作業也是並行安全的。 下表顯示常見並行安全的使用 concurrent_unordered_map 方法和運算子。

at

count

find

key_eq

begin

empty

get_allocator

max_size

cbegin

end

hash_function

operator[]

cend

equal_range

插入

size

雖然 count 方法可以從同時執行的執行緒安全地呼叫,如果新值同時插入容器,不同的執行緒可以接收不同的結果。

下表顯示常見不是並行安全的使用方法和運算子。

clear

max_load_factor

rehash

load_factor

operator=

交換

除了這些方法之外,從 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 物件視為削減變數。

當您的資源會供數個執行緒或工作共用時,combinable 類別會很有用。 combinable 類別可藉由以無法鎖定的方式提供對共用資源的存取,協助您消除共用狀態。 因此,這個類別提供另一種替代方式,來替代使用同步處理機制 (例如 Mutex) 同步處理多個執行緒對共用資料的存取。

方法和功能

下表顯示 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 類別

concurrent_unordered_map 類別

concurrent_unordered_multimap 類別

concurrent_unordered_set 類別

concurrent_unordered_multiset 類別

combinable 類別