ドナドナされるプログラマのメモ

Windows用アプリのプログラミングメモ

mozjpegをつかった画像変換ソフトの開発 その6

マルチスレッド関連の実装方法に迷いが生じたので、ちょっと考えを整理する。

■やりたいことは何か

やりたいことはスレッドからスレッドへの通知?いや違う。それは手段であって、目的ではない。やりたいのは、

  1. 処理Aが終了したあと、その結果を使う処理Bを起動する
  2. 処理Aが終了したあと、次の同様の処理A'を起動する
  3. 処理A'、処理Bはリソースが空くまで動作しない

である。つまり条件付きで処理を開始したい、ということになる。

これを実装するシンプルな方法は、こんな感じだろうか。

ThreadA(){ WaitForResource(); Do(); BeginThread(ThreadA); BeginThread(ThreadB);}

ThreadB(){ WaitForResource(); Do();}

この方式の問題点は、ThreadBの速度がThreadAよりも遅い場合である。リソースの空きを待つThreadBがどんどん増えてしまう。最悪、WindowsのHANDLE上限を超えるかもしれない。となると、ThreadBの実行状況に応じてThreadAの実行を制御する必要がある。ThreadAの実行制御はWaitForResource()が担っているので、これがThreadBの状況も監視するようになっていれば良さそうだ。

では、WaitForResource()はどのようなものなのだろうか?要件は以下だろう。

  1. 実行に必要なリソースの空きができるまで実行を停止する
  2. 後続処理の処理状況に空きができるまで実行を停止する
  3. 異なる複数のスレッドからの呼び出しに対応する

これはCSemaphore(セマフォ)で実現できそうだなあ。

■書き込みと読み込みの同時実行防止

書き込みと読み込みを同時に実行すると、遅くなる気がする。なので排他処理にしたい。そして、できれば書き込みを優先したい。これをセマフォでやるには、どうすればいいのだろう?まずは、優先を考えずに擬似コードを書いてみる。

CSemaphore hdd(initial=0,max=1);

CSemaphore CPU(initial=0,max=4);

ReadThread(){ Lock(hdd); ReadFile(); UnLock(hdd); BeginThread(CPUThread); BeginThread(ReadThread);}

CPUThread(){ Lock(CPU); Calc(); UnLock(CPU); Lock(hdd); WriteFile(); UnLock(hdd);}

このコードはデッドロックは起きなさそうだがWriteFile待ちのCPUThreadが大量にできそうだ。あまりよくない。

ならば、ReadThreadの開始に制限を追加したこれならどうか?

ReadThread(){ Lock(CPU); Lock(hdd); ReadFile(); UnLock(hdd); BeginThread(ReadThread); Calc(); Lock(hdd); WriteFile();UnLock(hdd); UnLock(CPU);}

 なお、CPUの数だけReadThreadができうるので、CPUThreadはReadThreadと統合してみた。すっきりはしたが、CPUの空きができるまでファイルを読み込まないというのは悲しい。となると、Lock(CPU)の位置を調整すればいいのか?ファイルを読み込んでからCPU実行待ち状態にしよう。次のファイル読み込みスレッドも、CPUリソースが空くまで待つか。

ReadThread(){ Lock(hdd); ReadFile(); UnLock(hdd); Lock(CPU); BeginThread(ReadThread); Calc(); Lock(hdd); WriteFile();UnLock(hdd); UnLock(CPU);}

 だいぶ良くなった。だがしかし、複数のスレッドがたまたま同時に終了した場合、こんどはReadFileが間に合わなくなりCPUが暇してしまう。できればCPUコア数分ぐらいは先読みしておきたい。ということは、読み込みスレッドのBeginThread実行条件とCPU計算実行条件を分ける必要があるのか。こんな感じ?

CSemaphore ReadBuff(initial=0, max=4+4);

ReadThread(){ Lock(hdd); ReadFile(); UnLock(hdd); Lock(ReadBuff); BeginThread(ReadThread); Lock(CPU); Calc(); Lock(hdd); WriteFile();UnLock(hdd); UnLock(CPU); UnLock(ReadBuff); }

 UnLock(ReadBuff)の位置はもっと前でもデッドロックせずに動作するが、Lock(hdd);より後ろとすることでWriteFileがReadFileよりも先に実行されるはず。うん、これでOKかな。あとは実装だ。