Posted at

マルチプラットフォーム対応のスレッド処理を書く

本文書は、マルチコア対応 Forth 系言語 Paraphrase の開発作業からのスピンオフ企画です(C++ 教材としての Paraphrase / Paraphrase 解体新書プロジェクト)。


お急ぎの方へ

 GitHub に揚げてあるコード https://github.com/iigura/Paraphrase/blob/master/src/inc/thread.h を見て下さい。


背景および戦略

 Paraphrase は、シングルソース・マルチプラットフォームな開発を行っています。現在対応しているプラットホームは Mac, Linux そして Windows です。Mac や Linux では pthread が使用できますが、Windows の場合、標準の環境では使用できません。そのため、プラットホーム依存の部分を適当なコード(マクロ)でラッピングし、ラッパーを使うことでプラットホーム依存の部分を吸収するようにしました(って、ごく普通の戦略ですね…)。


とは言うものの…

 Mac、Linux については pthread を使えば良いのですが、Windows については、さてどうしたものか、という状況でした。Windows 版 pthread もなんだか微妙な雰囲気な雰囲気を受けました(完全に個人的な偏見ですが)。


マルチスレッド対応 - 特に pthread_cond_wait() をどうすれば良いのか問題

 Windows 版を考えるときに、thread の生成などは Windows API でもなんとかなりそうな感じだったのですが、pthread_cond_wait() と同等のものが見つけられず色々調べておりました。

 結論から言えば、今や C++ ではスレッドは標準的な機能としてライブラリが提供されているので、それを利用することとした。いやぁ、時代は変わるもんですねぇ。灯台下暗し。

結局、


  • スレッドは std::thread を

  • シグナル待ちは std::condition_variable を

使用するようにしました。特に、Windows で pthread_cond_wait() したい場合、そこそこ速度が出そうな(手軽な *1)方法が見当たらないので、これは非常に助かっりました。

*1 私の記憶では、そのような場合は IOCP (I/O Completion Port)を用いるのが最速だったような気がするのだけれども…。ただ、それを用いて pthread の互換 wrapper を書くのも辛い年齢なので、その部分に関しては頑張ることはできませんでした。Windows ネイティブの機能で(簡単に)シグナル待ちする場合って、イベントを用いて WaitForSingleObject() するしかないんですかねぇ…だれか詳しい人、教えて下さい。

余談: std::condition_variable は遅い、という話もあり(要出典)、Paraphrase のビルドシステムにおいては、Mac と Linux 版はビルド時のオプションで pthread と std::thread を切り替えられるようにしてあります。


ラッパー(というかマクロ)

 https://github.com/iigura/Paraphrase/blob/master/src/inc/thread.h をダウンロードし、適当に include して下さい。すると、以下のような関数やマクロが定義されます。


  • Thread - スレッドを表す型

  • Mutex - 排他制御用ミューテックスを表す型

  • Cond - 条件(シグナル待ち等に使う)を表す型

…って、そのままですね(汗

以下、ラッパーです。th は Thread 型の変数を、mtx は Mutex 型の変数、cnd は Cond 型の変数を表すものとします:


  • initMutex(mtx) - mtx を初期化します

  • initCond(cnd) - cnd を初期化します

  • Lock(mtx) - mtx を用いてクリティカルセクション(排他制御エリア)に入ります

  • Unlock(mtx) - ロックに使用した mtx を用いてクリティカルセクションから抜けます

  • NotifyOne(cnd) - cnd を用いてシグナル待ちしているスレッドをひとつ起こします

  • NotifyAll(cnd) - cnd を用いてシグナル待ちしている全てのスレッドを起こします

  • CondWait(cnd,mtx) - cnd と mtx を用いてシグナル待ち状態に入ります(NotifyOne もしくは NotifyAll にて起こされます)

  • NewThread(th,func,arg) - 新しいスレッドを関数 func(arg) と共に起動します。起動したスレッドは th に代入されます

  • Yield() - 他のスレッドに yeild します


おわりに

 本コードと同様の働きをするラッパーライブラリもいくつかあったかと思いますが、どれも機能が豊富すぎて(=規模が大きくて)、結局自作してしまったという感じです。ぱっとみて分かる、小規模なものが欲しい方が、私の他にも存在するならば幸いです。