スレッドセーフな動的配列: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コンテナを正しく初期化してください。これはどのコンテナにも共通しており、以降解説するコンテナにも同じことが当てはまります。

