HEPtech     - マルチスレッドユーティリティー:TThreadUtilクラス -
トップページ | Tips | Topics
<マルチスレッドユーティリティー:TThreadUtilクラス>

 ここでは、ROOTでのマルチスレッド処理の簡単な実装を目的として開発中の マルチスレッドユーティリティー:TThreadUtilクラスの紹介と配布を行っています。

 TThreadUtilクラスは、通常のシングルスレッドでコーディングしたものに、 大きな変更を加える事なく簡単にマルチスレッド処理を実現するユーティリティークラスです。 ROOTはインタラクティブ・セッションではマルチスレッド処理は実装されていないので、 コンパイルしての実行が前提となっています。

 主な開発動機は、コード開発の初期〜中期における開発の効率化にあります。 最初期のスクラッチからコードを書き始める場合は、処理するイベント数やループ回数を極端に短くして動くコードを書けば良いでしょう。 終盤でデータの大量生成や、大量のデータ解析により結果の精度を求める場合には、 多くの計算機で構成されるクラスタやグリッドで複数のジョブを処理すればよいでしょう。 ところがコード開発の中期においては、最小単位のデータセットを処理してその結果を見ながらコードを書き進めるという場合があると思います。 この時、処理の複雑化やデータセットの肥大などによりしばしば数分単位の時間を要する事もあります。 この処理時間は、開発時間に占める完全なデッドタイムになります。 マルチコア、マルチCPUのマシンが割と普及してきた今日、マルチスレッド化による前述の処理時間の短縮は、 中期の開発効率の大幅な改善につながると考えます。 そこで、このTThreadUtilクラスは、マルチスレッドを意識せずに書いたシングルスレッドのコードでも、 大きな変更を加える事なく簡単に移植できるように設計しています。 以下ではこのTThreadUtilクラスについて説明します(TThreadUtil v0.0.2-a1_20090201版に準拠)。

TThreadUtilの簡単な使い方は以下のような感じになります。

#include "TThreadUtil.C" // TThreadUtil class
#include "anaClass.C" // analysis class for each thread
#include "TUtil.C" // Utility class for ROOT

int main(void){
    const int nThread=2; // the number of threads
    TThreadUtil<anaClass>* tu=new TThreadUtil<anaClass>(nThread);
    tu->Run();
    
    TUtil* u=new TUtil;
    tu->MergeObjects();
    std::vector<TH1*> histArray=tu->GetHistArray();
    //std::vector<TTree*> treeArray=tu->GetTreeArray();
    //std::vector<TGraph*> graphArray=tu->GetGraphArray();
    
    for ( int i=0;i<(int)histArray.size();i++ ){
        u->Draw(histArray[i]);
    }
    
    delete u;
    delete tu;
    return 0;
}

各スレッドで処理したい具体的内容は同封してある anaClass.C を加筆、或はそれを継承したクラスにより記述します。 TThreadUtil::MergeObjects()で、各スレッドで詰めたHistgram, Tree, Graphなどのマージを行い、 TThreadUtil::GetXXXArray()でそれらにアクセスできます。

 次に同封の anaClass について説明します。 このクラスは1つのスレッドが行う具体的な処理を記述します。 以下に anaClass の簡単な使用例を示します(TTreeでの解析の例)。

anaClass::anaClass(int threadID, TTree* tr):m_thID(threadID),m_tr(tr){
    m_tr->SetBranchAddress("x",&m_x);
}

void anaClass::ProcessLoop(void){
    TH1D* hist=new TH1F(Form("hist_%d",m_thID),"",10,0.,10.);
    m_histArray.push_back(hist);
    
    int nEvent=m_tr->GetEntriesFast();
    for ( int i=0;i<nEvent;i++ ){
        m_tr->GetEntry(i);
        ...
    }
}

メンバー関数のProcessLoopを見てください。 ここでの注意点としては、ヒストグラム等を生成する場合に、他のスレッドで生成するオブジェクトと名前が重複しないように、 名前にスレッドIDを付け加えるなどの処置をしてください。 (ROOTは名前でオブジェクトを独自に管理している。) 複数あるスレッドの何番目のスレッドかを示すスレッドIDはコンストラクタで m_thID に格納されています。 次に生成したオブジェクトを vector に詰めて、外からアクセスできるようにしておきます。 こうしておくと、TThreadUtil::MergeObjects や TThreadUtil::GetXXXArray でマージ、アクセスできます。 Treeのマルチスレッド処理の場合、スレッドの数に分割されたTreeがこのクラスに渡されているので、 メインのループ処理などは普通どおりに記述する事ができ、既存のシングルスレッドのコードもそのまま移植する事ができます。 次にもう少し一般的な処理の場合について説明します。

anaClass::anaClass(int threadID, void* arg):m_thID(threadID),m_tr(0){
    m_loopN=(int)arg;
}

bool anaClass::GetNext(void* arg){
    bool loop=false;
    TThread::Lock();
  
    if ( currentN++ < m_loopN ){
        Hoge* t_hoge=(Hoge*)arg;
        t_hoge->foo=bar;
        loop=true;
    }
  
    TThread::UnLock();
    return loop;
}

void anaClass::ProcessLoop(void){
    TH1D* hist=new TH1F(Form("hist_%d",m_thID),"",10,0.,10.);
    m_histArray.push_back(hist);
    
    Hoge* hoge=new Hoge;
    while ( GetNext((void*)hoge) ){
        double par=hoge->foo;
        ...
    }
}

 まずコンストラクタについてです。 TThreadUtil::ThreadUtil(int nThread, void* arg) のタイプを呼んだ場合、 ここで渡した二つ目の引数はそのままこのクラスに渡されており、ここで受け取る事ができます。 この例では、ループ回数を受け取っています。 次にGetNextメソッドについて。 このメソッドは、全スレッド共通のループのカウントアップと、各ループの初期設定の受け取りを行います。 変数 currentN は static で各スレッドから同じ場所を参照します。 複数のスレッドが同時に同じ変数にアクセスする事は危険なので、 処理の前後で TThread::(Un)Lock を行い、この区間は1スレッドずつしかアクセスできないようにしています。 しかしこの最初2行と最終2行は配布している anaClass.C に元々記述されているので問題ありません。 そして引数で渡された構造体/クラスへのポインタの先に、次のループの設定を詰めて返します。 次にProcessLoopメソッドについてです。 ヒストグラムなどのオブジェクトをスレッドIDなどを含む名前で生成し、vector に詰めておくのは先と同様です。 GetNextはループを継続するか否かのbool値を返すので、それをWhileでまわします。 各ループの設定は、前述のようにGetNextへ渡す引数を通して受け取ります。 注意点としては、GetNextメソッドの仕様上、ProcessLoopが非常に簡単な処理で、 GetNextを頻繁に多数回呼ぶようなプログラムはあまりマルチスレッドの恩恵を受ける事ができません。 逆にProcessLoopが時間を要する非常に重い処理で、 GetNextを呼ぶ回数があまり多くない場合には、理想的なマルチスレッド処理が行われます。

 制限事項としては、各スレッドで同時にFittingを行う事はできません。 これはROOTがFitメソッドの中で、TVirtualFitter::GetFitter()などのstaticな関数にアクセスする仕様のせいと思われます(ROOT v5.18以前?)。 ROOT v5.22以降で Fitメソッドに大規模な変更が加えられているようなので、近い将来対応できるかもしれません。 とりあえず現状での対処療法としては毎度毎度Fittingをせずに、ある一定数溜まった後、前後を TThread::(Un)Lock で挟んでまとめてFitを行う、 またはFittingを行いたいオブジェクトをスタックに溜めて、Fitting専門のスレッドを立てるかなどだと思われますが、 どちらもオーバーヘッドが少なくないので、Fittingをメインに行う処理では思うほどの高速化は期待できないかもしれません。


 以下から TThreadUtilクラスをダウンロードする事ができます。 現時点では超開発中のアルファ版である事に十分ご留意の上お試しください。 最新版は、v0.0.2-a1_20090201 です。

 =>  download TThreadUtil.zip


 動作環境

 ROOT v5.22以降を推奨(v5.22/00でテストされています)。 v5.20でも動くかもしれませんが、v5.18では一部機能が動きません。 それ以前のバージョンでの試用はおすすめしません。 少なくとも ${ROOTSYS}/lib/libThread.so(dylib) が必要です。 以前はROOTコンパイル時に特別に指定する必要があったようですが、 最近バージョンでは特に指定しなくてもデフォルトで生成されている(?)ようです。


 更新履歴

v0.0.2-a120090201 TTree処理のマルチスレッド化の際に分割したTTreeの処理を変更
(この変更により大きなサイズのTTreeを扱えるようになった。)
v0.0.1-a620080704 ダミーなROOTファイルの取り扱いを修正
v0.0.1-a520080628 デストラクタでのオブジェクト削除をコメントアウト
v0.0.1-a4 TTree::CopyEntries(...)を呼ぶ前にTTree::CopyAddressesを追加
v0.0.1-a3 CopyTreeを呼ぶ際にダミーなファイルを開く仕様にした
v0.0.1-a2 void ThreadFunc を void* ThreadFunc に変更
( 'undetached' なスレッドになり、取り扱いも変更 )
v0.1.0-a1 develop version


 構成ファイル

TThreadUtil.C : TThreadUtilクラスの定義ファイル(メイン)
anaClass.C : 各スレッド処理を記述するクラスの雛形/基底クラス
2005-2017 HEPtech All rights reserved. Link/Unlink free.
inserted by FC2 system