Yuniframe: SDL2のイベントキューとOSのカーネルキューを結合したい
これ思ってたより難しいな。。?
課題
libuvのような非同期I/OライブラリはOSカーネル側のキュー(epoll、kqueue、IOCP)を使う。libuvの場合は、スレッドは uv_run
関数を実行することでイベントを待ち受け、何かイベントが発生したらコールバックを呼ぶスタイルになっている。 uv_async_t
を使用して、外部のスレッドからイベント待ちをwakeupできる。
SDL2は独自のイベントキューを持っていて、 SDL_PollEvent
によってイベントを待ち合わせる(か、Emscriptenの場合はWebブラウザのanimationコールバックで待ち合わせることになる)。外部のスレッドからイベント待ちをwakeupするには SDL_PushEvent
を使う。
また、SDL固有の制約としてSDLのイベントキューは main
関数を呼んだスレッドでしか待ち受けることができないという問題がある。
... で、問題は単一のスレッドは SDL2のイベントキューか、libuvのイベントキューか、どちらかしか待てない という点となる。
libuvのイベント待ち "だけ" をサブスレッド化できるか問題
SDLのイベント待ちはメインスレッド以外では行えないので、サブスレッド化できるとしたらlibuvのイベント待ち および コールバックということになる。
libuvもスレッドセーフではない 。つまり、 uv_async_t
を使った操作以外は、単一の loop に紐付いた操作を他のスレッドから同時に行うことはできない。
... じゃぁ排他すれば良いのでは。。?つまり、以下のような役割分担を考える:
- メインスレッド: SDLやlibuvのAPIを呼んで処理をする + SDLのイベント待ちをする
- IOスレッド: libuvのイベント待ちをする + libuvのコールバックを呼ぶ
このとき、 libuvのAPIが呼ばれるチャンスがあるときはlibuvのイベント待ちをしない ことを徹底すれば、排他を実現できることになる。
- メインスレッド
while(1){
SDLのイベント待ち();
IOスレッドを起床させ、IOスレッドがイベント待ちから抜けるまで待つ(); /* ★ condvar */
while(未処理のイベントが有る()){
SDLのイベントとIOイベントを処理(); /* この中でSDLとlibuvのAPIが呼ばれる */
追加のSDLイベントが無いか確認();
追加のIOイベントが無いか確認(); /* libuvのコールバックはメインスレッドで呼ばれる */
}
起床要求フラグを立てる(); /* ★ mutex */
IOスレッドを起床させ、イベント待ちを再開させる(); /* ★ async */
}
- IOスレッド
while(1){
if(イベント待ちを許可){
while(メインスレッドから起床されるまで()){ /* ★ mutex */
IOイベントが無いか確認(); /* libuvのコールバックはIOスレッドで呼ばれる */
IOイベントをメインスレッドのイベントキューに追加();
}
/* SDLから起床された */
イベント待ちを禁止();
起床要求フラグを落とす(); /* ★ mutex */
メインスレッドにイベント待ちが終了したことを通知(); /* ★ condvar */
}else{
メインスレッドから起床要求を待つ(); /* ★ condvar */
if(メインスレッドからスレッド終了要求が来た()){
IOオブジェクトを全部closeして捨てる();
メインスレッドに終了処理完了を通知();
break; /* スレッドを終了 */
}
if(メインスレッドからイベント待ち開始許可が出た()){
イベント待ちを許可();
}
}
}
このとき、libuvに渡したコールバックはIOスレッドとメインスレッドの両方で呼ばれる可能性があるが、複数のスレッドから同時に呼ばれる可能性は無い。このため、特にスレッドセーフにしなければならないということは無い。
一般には、異なるスレッドで実施した変数の変更が他のスレッドから直ちに参照できる保証は無いが、スレッドの待合せを行う ★ のところではメモリバリアが張られることになるので問題ない。 ... いやまぁ、 ★ async は保証は無いんで別途mutexは要るけど。