ワークの分離

インテル® TBB では、タスクグループの完了を待つスレッドがほかの利用可能なタスクを実行することがあります。特に、並列構造が別の並列構造を呼び出した場合、スレッドは内側レベルの構造の完了を待つ間、外側レベルの構造からタスクを取得することができます。

次の 2 つの parallel_for 呼び出しの例では、2 つ目の (入れ子の) 並列ループの呼び出しが最初の (内側の) ループ反復の実行をブロックしています。

// 最初の並列ループ
tbb::parallel_for( 0, N1, []( int i ) { 
    // 2 つ目の並列ループ
    tbb::parallel_for( 0, N2, []( int j ) { /* 処理を行う */ } );
} );

ブロックされたスレッドは最初の並列ループに属するタスクを処理することができます。その結果、外側のループの 2 つ以上の反復が同じスレッドに同時に割り当てられることがあります。つまり、インテル® TBB では、並列構造を構成する関数の実行は、シングルスレッド内でも順序関係がありません。スレッドで利用可能な並列処理を制限しないため、ほとんどの場合、この動作は無害または有益です。

しかし、場合によっては、順序関係のない実行によりエラーが発生することがあります。例えば、スレッドローカル変数は、入れ子の並列構造の後に予期せず変数の値を変更することがあります。

tbb::enumerable_thread_specific<int> ets;
tbb::parallel_for( 0, N1, [&ets]( int i ) {
    // スレッド固有の値を設定
    ets.local() = i;
    tbb::parallel_for( 0, N2, []( int j ) { /* Some work */ } );
    // 上記の parallel_for の実行中に、スレッドは外側の parallel_for
    // 反復を実行してスレッド固有の値を変更することがある
    assert( ets.local()==i ); // その場合はアサーションに失敗する
} );

ほかのシナリオでは、この動作によりデッドロックやその他の問題が発生することがあります。これらのケースでは、スレッド内で実行順序を確実に保証することを推奨します。インテル® TBB は、タスクが並列に実行されているほかのタスクに干渉しないように、並列構造の実行を分離する方法を提供します。

その 1 つの方法は、内部レベルのループを個別の task_arena で実行します。

tbb::enumerable_thread_specific<int> ets;
tbb::task_arena nested;
tbb::parallel_for( 0, N1, [&]( int i ) {
    // スレッド固有の値を設定
    ets.local() = i;
    nested.execute( []{
        // 内部の parallel_for を個別の領域で実行して、スレッドが
        // 外側の parallel_for のタスクを処理しないようにする
        tbb::parallel_for( 0, N2, []( int j ) { /* 処理を行う */ } );
    } );
    assert( ets.local()==i ); // 有効なアサーション
} );

しかし、ワークの分離のために個別の領域を使用することは必ずしも便利であるとは限りません。また、オーバーヘッドが発生することもあります。そのため、インテル® TBB の this_task_arena::isolate 関数は、ファンクターの有効範囲 (分離領域とも呼ばれます) でスケジュールされたタスクのみを処理するように呼び出しスレッドを制限することで、ユーザー定義ファンクターを分離して実行します。

タスク待機呼び出し、または分離された領域内部のブロッキング並列構造に入ると、スレッドはその領域内でスポーンされたタスクまたはその子タスク (ほかのスレッドによりスポーンされる) のみ実行できます。スレッドは、外側レベルのタスクまたはほかの分離された領域に属するタスクを実行することはできません。

分離領域は、その領域を呼び出したスレッドにのみ制限を適用します。同じタスク領域内で実行しているほかのスレッドは、this_task_arena::isolate を個別に呼び出して分離しない限り、タスク選択の制限を受けません。

次のサンプルは、this_task_arena::isolate を使用して、入れ子の並列構造の呼び出し中にスレッドローカル変数が予期せず変更されないことを保証しています。

#include "tbb/task_arena.h"
#include "tbb/parallel_for.h"
#include "tbb/enumerable_thread_specific.h"
#include <cassert>

int main() {
    const int N1 = 1000, N2 = 1000;
    tbb::enumerable_thread_specific<int> ets;
    tbb::parallel_for( 0, N1, [&ets]( int i ) {
        // スレッド固有の値を設定
        ets.local() = i;
        // 現在のスレッドが外側の並列ループのタスクを処理しないように
        // 2 つ目の並列ループを分離された領域で実行
        tbb::this_task_arena::isolate( []{
            tbb::parallel_for( 0, N2, []( int j ) { /* 処理を行う */ } );
        } );
        assert( ets.local()==i ); // 有効なアサーション
    } );
    return 0;
}

関連情報