スレッドセーフな動的配列:concurrent_vector
プログラミングをしていると、動的にサイズを変えられる配列が便利な場面が多々あります。従来のC++プログラミングでは、動的配列としてvector
コンテナを使用しましたが、TBBによる並列プログラミングではconcurrent_vector
を使用します。
concurrent_vector
は並列用に作られたものなので、vector
と少し違いますが、よく似ているのですぐに使えるようになると思います。サンプルプロジェクトVector
とConcurrent_Vector
を用意したので見てください。使い方そのものは簡単なので、サンプルを見れば基本的な仕組みは分かると思います。
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main(void) { //char型の配列 char ch[] = { "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }; //C形式文字列末尾を除外したサイズ int size = ( sizeof( ch ) / sizeof( char ) ) - 1; //char型のvectorを用意する vector<char> v1; for ( int i = 0; i < size; ++i ) v1.push_back( ch[ i ] ); //viの要素を表示 cout << "初期要素:"; copy( v1.begin(), v1.end(), ostream_iterator<char>( cout, " " ) ); cout << endl; //要素を削除して表示 v1.erase( v1.begin() + 5, v1.end() ); cout << "F以降を削除した後の要素:"; copy( v1.begin(), v1.end(), ostream_iterator<char>( cout, " " ) ); cout << endl; //要素をコピー vector<char> v2( v1 ); cout << "コピーした要素:"; copy( v2.begin(), v2.end(), ostream_iterator<char>( cout, " " ) ); cout << endl; //任意の要素を追加 cout << "要素のサイズ:" << v2.size() << endl; cout << "要素XYZを追加:"; const char* string = "XYZ"; size_t strSize = strlen( string ); for ( int i = 0; i < strSize; ++i ) v2.push_back( string[ i ] ); copy( v2.begin(), v2.end(), ostream_iterator<char>( cout, " " ) ); cout << endl; cout << "要素のサイズ:" << v2.size() << endl; //領域を拡張 cout << "領域を2文字ぶん拡張します・・・" << endl; v2.resize( v2.size() + 2, ' ' ); cout << "要素のサイズ:" << v2.size() << endl; //要素を変更 cout << "最後の2文字を@@に変更:"; v2[ v2.size() - 2 ] = '@'; v2[ v2.size() - 1 ] = '@'; copy( v2.begin(), v2.end(), ostream_iterator<char>( cout, " " ) ); cout << endl; //終了 cout << endl << endl; return 0; }
#include <iostream> #include "tbb/concurrent_vector.h" #include <algorithm> using namespace std; using namespace tbb; int main(void) { //char型の配列 char ch[] = { "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }; //C形式文字列末尾を除外したサイズ int size = ( sizeof( ch ) / sizeof( char ) ) - 1; //char型のvectorを用意する concurrent_vector<char> v1; for ( int i = 0; i < size; ++i ) v1.push_back( ch[ i ] ); //viの要素を表示 cout << "初期要素:"; copy( v1.begin(), v1.end(), ostream_iterator<char>( cout, " " ) ); cout << endl; //要素を削除して表示 v1.clear(); for ( int i = 0; i < 5; ++i ) v1.push_back( ch[ i ] ); cout << "F以降を削除した後の要素:"; copy( v1.begin(), v1.end(), ostream_iterator<char>( cout, " " ) ); cout << endl; //要素をコピー concurrent_vector<char> v2( v1 ); cout << "コピーした要素:"; copy( v2.begin(), v2.end(), ostream_iterator<char>( cout, " " ) ); cout << endl; //任意の要素を追加 cout << "要素のサイズ:" << v2.size() << endl; cout << "要素XYZを追加:"; const char* string = "XYZ"; size_t strSize = strlen( string ); std::copy( string, string + strSize, v2.grow_by( strSize ) ); copy( v2.begin(), v2.end(), ostream_iterator<char>( cout, " " ) ); cout << endl; cout << "要素のサイズ:" << v2.size() << endl; //領域を拡張 cout << "領域を2文字ぶん拡張します・・・" << endl; v2.grow_to_at_least( v2.size() + 2 ); cout << "要素のサイズ:" << v2.size() << endl; //要素を変更 cout << "最後の2文字を@@に変更:"; v2[ v2.size() - 2 ] = '@'; v2[ v2.size() - 1 ] = '@'; copy( v2.begin(), v2.end(), ostream_iterator<char>( cout, " " ) ); cout << endl; //終了 cout << endl; return 0; }
サンプルを見比べると、vector
とconcurrent_vector
は非常に似ていますが、違いが3点あることが分かります。この部分がconcurrent_vector
の特性を表しています。
1つ目の違いは、concurrent_vector
の方が削除が不便である点です。vector
では、erase
メソッドを使用して簡単に指定範囲の要素を削除できます。一方concurrent_vector
はclear
メソッドを使用して一度すべての要素を削除してから、残すべき要素を追加する必要があります。この理由はconcurrent_vector
が並列時のパフォーマンス向上を目指して作られているからです。並列処理時に要素の削除を認めてしまうと、内部に保持している要素を移動しなくてはなりません。これを行うには広い範囲のロックをする必要性が生じ、パフォーマンスに悪影響を及ぼします。そのため、concurrent_vector
は基本的に追加するのみと考えたらよいでしょう。なお、clear
メソッドは並列処理用ではないので、絶対にこのメソッドを並列的に実行しないでください。
2つ目の違いは、要素の追加の仕方が違うところです。並列処理ではコンテナは同時に複数のスレッドからメソッドが呼び出されます。従って、コンテナが保持する領域を安全に拡張する手段が必要となります。そのためのメソッドがgrow_by
メソッドとgrow_to_at_least
メソッドです。grow_by
メソッドは、指定数ぶんの連続する要素をベクトルに安全に追加して、最初に追加した要素のイテレータ(反復子)を返します。TBB2.0ではint型のインデックス値を返すようになっていましたが、今ではイテレータを返すように変更されています。十分に注意してください。
3つ目の違いは、リサイズの方法が違う点です。先ほど少し触れましたが、並列処理時に安全に領域を拡張するためgrow_to_at_least
メソッドが用意されています。このメソッドを使用すると、複数のスレッドからコンテナがアクセスされても安全に領域を拡張できます。
最後に並列処理時に注意するべきことを書きます。それは、concurrent_vector
の構築中は、並列的にアクセスしてはならないことです。concurrent_vector
はスレッドセーフなコンテナですが、初期化処理を正しく行う前では別です。そのため、並列処理をする前にconcurrent_vector
コンテナを正しく初期化してください。これはどのコンテナにも共通しており、以降解説するコンテナにも同じことが当てはまります。