タスクの分離

インテル® 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 ) { /* 処理を行う */ } );
    // 上記の parallel_for の実行中に、スレッドは外側の parallel_for の
    // タスクを処理してスレッド固有の値を変更することがある
    assert( ets.local()==i ); // その場合はアサーションに失敗する
} );

ほかのシナリオでは、デフォルトのインテル® TBB の動作によりデッドロック、スタック・オーバーフロー、その他が発生することがあります。そのため、スレッド内の順序での実行を保証することを強く推奨します。この保証をインテル® 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( []{
        tbb::parallel_for( 0, N2, []( int j ) { /* 処理を行う */ } );
    } );
    // 上記の parallel_for は、別の領域で実行され、スレッドが外側の
    // parallel_for のタスクを処理しないようにしている
    assert( ets.local()==i ); // 有効なアサーション
} );

しかし、ワークの分離のために個別の領域を使用することは必ずしも便利であるとは限りません。また、オーバーヘッドが発生することもあります。これらの欠点を克服するため、インテル® TBB では this_task_arena::isolate 関数を追加しました。

this_task_arena::isolate 関数サマリー

ユーザー定義関数オブジェクトを現在のタスク領域内で分離して実行する関数。

ヘッダー

#define TBB_PREVIEW_TASK_ISOLATION 1
#include "tbb/task_arena.h"

構文

namespace tbb {
    namespace this_task_arena {
        template<typename F> void isolate( const F& f );
    }
}

説明

this_task_arena::isolate は、ファンクターの有効範囲 (分離領域とも呼ばれます) でスケジュールされたタスクのみ処理するように呼び出しスレッドを制限することにより、ユーザー定義ファンクターを分離して実行します。

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

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

注意

フローグラフや task_group のような非同期並列構造は、分離された領域内で注意して使用する必要があります。分離して実行される graph::wait_for_alltask_group::wait では、フローグラフのノードで try_put を呼び出して (または task_group::run で) スケジュールされたタスクは、同じ分離領域 (またはその領域で以前にスポーンされたタスク) でスケジュールされた場合にのみアクセスできます。その他の場合にアクセスすると、パフォーマンス問題やデッドロックが発生します。

この関数を使用するには、アプリケーションをプレビュー・ライブラリーとリンクする必要があります。

サンプル

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

#define TBB_PREVIEW_TASK_ISOLATION 1
#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;
}

関連情報