質問

カスタムスレッドプールクラスがあり、それぞれが独自のイベント(信号)で待つスレッドを作成します。新しいジョブがスレッドプールに追加されると、最初のフリースレッドを目指してジョブを実行します。

問題は次のとおりです。それぞれの約10'000の反復の約1000ループがあります。これらのループは順次実行する必要がありますが、4つのCPUを使用できます。私がやろうとしているのは、10'000イテレーションループを4 2'500イテレーションループ、つまりスレッドごとに1つに分割することです。しかし、次の「大きな」イテレーションに行く前に、4つの小さなループが終了するのを待たなければなりません。これは、仕事を束ねることができないことを意味します。

私の問題は、スレッドプールと4つのスレッドを使用することは、ジョブを順番に実行するよりもはるかに遅くなることです(個別のスレッドで実行される1つのループを使用すると、メインスレッドで順番に実行するよりもはるかに遅くなります)。

私は窓にいるので、イベントを作成します CreateEvent() そして、それらの1つを使用して待ちます WaitForMultipleObjects(2, handles, false, INFINITE) メインスレッドが呼び出すまで SetEvent().

このイベント全体が(重要なセクションを使用してスレッド間の同期とともに)かなり高価であるように見えます!

私の質問は、イベントを使用するには「多くの」時間がかかるのは普通ですか?もしそうなら、私が使用できる別のメカニズムがあり、それは時間が少ないでしょうか?

説明するコードを次に示します(スレッドプールクラスからコピーされた関連部品):

// thread function
unsigned __stdcall ThreadPool::threadFunction(void* params) {
    // some housekeeping
    HANDLE signals[2];
    signals[0] = waitSignal;
    signals[1] = endSignal;

    do {
        // wait for one of the signals
        waitResult = WaitForMultipleObjects(2, signals, false, INFINITE);

        // try to get the next job parameters;
        if (tp->getNextJob(threadId, data)) {
            // execute job
            void* output = jobFunc(data.params);

            // tell thread pool that we're done and collect output
            tp->collectOutput(data.ID, output);
        }

        tp->threadDone(threadId);
    }
    while (waitResult - WAIT_OBJECT_0 == 0);

    // if we reach this point, endSignal was sent, so we are done !

    return 0;
}

// create all threads
for (int i = 0; i < nbThreads; ++i) {
    threadData data;
    unsigned int threadId = 0;
    char eventName[20];

    sprintf_s(eventName, 20, "WaitSignal_%d", i);

    data.handle = (HANDLE) _beginthreadex(NULL, 0, ThreadPool::threadFunction,
        this, CREATE_SUSPENDED, &threadId);
    data.threadId = threadId;
    data.busy = false;
    data.waitSignal = CreateEvent(NULL, true, false, eventName);

    this->threads[threadId] = data;

    // start thread
    ResumeThread(data.handle);
}

// add job
void ThreadPool::addJob(int jobId, void* params) {
    // housekeeping
    EnterCriticalSection(&(this->mutex));

    // first, insert parameters in the list
    this->jobs.push_back(job);

    // then, find the first free thread and wake it
    for (it = this->threads.begin(); it != this->threads.end(); ++it) {
        thread = (threadData) it->second;

        if (!thread.busy) {
            this->threads[thread.threadId].busy = true;

            ++(this->nbActiveThreads);

            // wake thread such that it gets the next params and runs them
            SetEvent(thread.waitSignal);
            break;
        }
    }

    LeaveCriticalSection(&(this->mutex));
}
役に立ちましたか?

解決

ループを並列化し、VS 2008を使用している場合は、OpenMPを見ることをお勧めします。 Visual Studio 2010 Beta 1を使用している場合、 平行パターンライブラリ, 、特に "for for" / "apisの並列 または 「タスクグループ クラスは、おそらくあなたがやろうとしていることを行う可能性が高いため、コードが少ないだけです。

パフォーマンスについてのあなたの質問に関して、ここでは本当に依存します。反復中にスケジュールする作業の量と費用を確認する必要があります。 waitformultipleObjectsは、あなたがそれをたくさんヒットし、あなたの仕事が小さい場合、非常に高価になる可能性があります。そのため、すでに構築された実装を使用することをお勧めします。また、デバッグモードでデバッグモードで実行していないこと、およびタスク自体がロック、I/O、またはメモリの割り当てをブロックしておらず、誤った共有にヒットしていないことを確認する必要があります。これらのそれぞれには、スケーラビリティを破壊する可能性があります。

これをプロファイラーのような下で見ることをお勧めします xperf Visual Studio 2010 Beta 1のF1プロファイラー(競合を見るのに役立つ2つの新しい並行性モードがあります)またはIntelのVtune。

タスクで実行されているコードを共有することもできます。そのため、人々は自分が何をしているのかをよりよく理解することができます。なぜなら、私が常にパフォーマンスの問題で得られる答えは、最初の「それは依存している」と2番目にそれを紹介しました。」

幸運を

- リック

他のヒント

これは、プロデューサーの消費者パターンとして私に見えます。これは、2つのセマフォで埋め合わせて、1つはキューオーバーフローを守り、もう1つは空のキューを守ることができます。

いくつかの詳細を見つけることができます ここ.

はい、 WaitForMultipleObjects かなり高価です。あなたの仕事が小さい場合、あなたが見ているように、頭上の同期は実際に仕事をするコストを圧倒し始めます。

これを修正する1つの方法は、複数のジョブを1つにバンドルすることです。「小さな」ジョブ(ただし、そのようなことを評価する)を取得した場合は、1つの合理的なサイズのジョブを作るのに十分な小さなジョブが一緒になるまでどこかに保存します。次に、それらすべてを処理のためにワーカースレッドに送ります。

あるいは、シグナリングを使用する代わりに、マルチリーダーのシングルライターキューを使用してジョブを保存できます。このモデルでは、各ワーカースレッドはキューからジョブをつかもうとします。それが見つかったとき、それは仕事をします。そうでない場合は、短期間眠り、目を覚まして再び試みます。これにより、タスクごとのオーバーヘッドが低下しますが、作業がない場合でもスレッドはCPUを取り上げます。それはすべて、問題の正確な性質に依存します。

気をつけてください、あなたはまだEndignalが放出された後、まだ次の仕事を求めています。

for( ;; ) {
    // wait for one of the signals
    waitResult = WaitForMultipleObjects(2, signals, false, INFINITE);
    if( waitResult - WAIT_OBJECT_0 != 0 )
        return;
    //....
}

それほど高価ではないはずですが、仕事にまったく時間がかからない場合、スレッドと同期オブジェクトのオーバーヘッドが重要になります。このようなスレッドプールは、より長い処理ジョブやCPUリソースの代わりに多くのIOを使用するジョブにとってはるかに優れています。ジョブの処理時にCPUバウンドである場合は、CPUごとに1つのスレッドしかないことを確認してください。

他の問題があるかもしれませんが、GetNextJobはどのようにしてデータを処理しますか?大量のデータコピーがある場合、オーバーヘッドを再び大幅に増やしました。

キューが空になるまで、各スレッドにキューからジョブを引っ張り続けることにより、最適化します。そうすれば、100個のジョブをスレッドプールに渡すことができ、同期オブジェクトはスレッドをキックオフするために一度だけ使用されます。また、ジョブをキューに保存し、データをコピーするのではなく、ポインター、参照またはイテレーターをスレッドに渡します。

スレッド間のコンテキストの切り替えも高価になる場合があります。場合によっては、1つのスレッドまたは複数のスレッドでジョブを順番に処理するために使用できるフレームワークを開発することは興味深いことです。このようにして、2つの世界の中で最高のものを持つことができます。

ちなみに、あなたの質問は正確に何ですか?より正確な質問でもっと正確に答えることができます:)

編集:

イベントの部分は、場合によっては処理よりも多く消費できますが、処理が達成するのが非常に速い場合を除き、それほど高価ではありません。この場合、スレダを切り替えることも高価です。したがって、私の答えは、物事を順番に行うことについての最初の部分です...

スレッド間同期ボトルネックを探す必要があります。スレッドをトレースすることができます。

編集:さらにヒントの後...

正しく推測すると、あなたの問題は、すべてのコンピューターコア/プロセッサを効率的に使用して、いくつかの処理のエッセンシャリのシーケンシャルを偏差化することです。

例のように計算するために4つのコアと10000ループを持っていることを考えてください(コメントで)。あなたは、4つのスレッドが終了する前に終了するのを待つ必要があると言いました。次に、同期プロセスを簡素化できます。 4つのスレッドをnth、nth+1、nth+2、nth+3ループを与えるだけで、4つのスレッドが完了してから進むのを待ちます。ランデブーまたはバリア(nスレッドが完了するのを待つ同期メカニズム)を使用する必要があります。 ブースト そのようなメカニズムがあります。効率のためにWindowsの実装を見ることができます。スレッドプールはタスクに本当に適していません。重要なセクションで利用可能なスレッドの検索は、CPU時間を殺しているものです。イベントの部分ではありません。

このイベント全体が(重要なセクションを使用してスレッド間の同期とともに)かなり高価であるように見えます!

「高価」は相対的な用語です。ジェットは高価ですか?車ですか?または自転車...靴...?

この場合、問題は次のとおりです。イベントは、ジョブ機能が実行するのにかかった時間と比較して「高価」ですか?それはいくつかの絶対的な数字を公開するのに役立ちます:「非読み取り」時にプロセスにはどれくらい時間がかかりますか?それは数ヶ月ですか、それともいくつかのフェムト秒ですか?

スレッドプールサイズを増やすと、時間はどうなりますか?プールサイズ1、次に2、その後4などを試してください。

また、過去にスレッドプールにいくつかの問題があるので、スレッド機能が実際に呼び出される回数を数えるためにデバッグを提案することをお勧めします...それはあなたが期待するものと一致しますか?

空中から姿を選ぶこと(ターゲットシステムについて何も知らずに、あなたが示していないコードで「巨大」なことをしていないと仮定すると)、私はそれぞれの「ジョブ」の「イベントのオーバーヘッド」が期待されると期待しています。マイクロ秒で測定します。たぶん百程度。 JobFunctionでアルゴリズムを実行するのにかかった時間が今回よりもそれほど大きくない場合、スレッドはそれを保存するのではなく、時間がかかる可能性があります。

あなたはそれがそうだと言うので 多くの 連続的な実行よりも並行して遅いので、内部2500ループイテレーションの処理時間は小さいと思います(少数のマイクロ秒範囲)。それから、あなたのアルゴリズムを確認して、より大きな歳差運動の塊を分割する以外にできることはあまりありません。 OpenMPは役に立たず、他のすべての同期技術も、すべてがイベントに依存しているため、どのように役立ちません(スピンループは適格ではありません)。

一方、2500ループの反復の処理時間が100マイクロ秒(現在のPCで)が大きい場合、ハードウェアの制限に遭遇する可能性があります。処理が多くのメモリ帯域幅を使用している場合、それを4つのプロセッサに分割しても、より多くの帯域幅が得られない場合、実際には衝突のために少なくなります。また、上位1000の反復のそれぞれが4つのコアのキャッシュを洗い流してリロードするキャッシュキャイクの問題にぶつかることもあります。その後、1つの解決策はありません。ターゲットハードウェアに応じて、何もない場合があります。

前述のように、スレッドによって追加されるオーバーヘッドの量は、定義した「ジョブ」を行うのにかかる時間の相対量に依存します。したがって、ピースの数を最小限に抑えますが、最後の計算グループが完了するのを待っているプロセッサのアイドル状態のままにしない作業チャンクのサイズのバランスを見つけることが重要です。

コーディングアプローチにより、新しい作業を提供するためのアイドルスレッドを積極的に探していることにより、オーバーヘッド作業の量が増加しました。オペレーティングシステムはすでにそれを追跡しており、より効率的に行うことを行っています。また、function threadpool :: addjob()は、すべてのスレッドが使用されており、作業を委任できないことを発見する場合があります。しかし、それはその問題に関連する返品コードを提供しません。何らかの方法でこの状態をチェックしておらず、結果のエラーに気付いていない場合、それは常にアイドルプロセッサがあることを意味します。 addjob()が名前が付けられていることを行うようにコードを再編成することをお勧めします。各ワーカースレッドが既存の作業で行われたときに積極的に新しい仕事を得る一方で、ジョブのみを追加します(誰が仕事をするのかを見つけることもありません)。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top