pthreadはクソだというのをひしひしと実感している…わけではないけど、 Win32のスレッドと比べると微妙な感じが拭えないので、Linuxのスレッドを使えばもっとハッピーになれるよ、というような話。
pthreadは Win32 のスレッドに比べていくつか微妙だと思われる点がある。まずはその点について書こう、と思ったのだけど、大体[ここ]に書いてあったので、列挙だけしておく。詳細はリンク先を読んでね!
他にもあるかも。timedwaitの時間がキモいとか。
と、そういうわけでなんだかLinuxがWin32に劣るみたいな感じになりそうなので、ここで、LinuxのスレッドがWin32スレッドに劣っていない、というような話を書いておく。
(標準に準拠しないAPIを使うデメリットは考えない)
futex、signalfdの順に見ていこう。
-- 超雑談。Windowsは何故かVistaになってから条件変数を用意[これ] --
Linuxでは、スレッド、プロセス間で同期をとるのに、futex (Fast Userspace muTexesの略…らしい) を使っている。
この futex というのは、ひとことでいうと、メモリアドレスをキーとして待ちキューを操作する方法…とでも言うべきか。(わかりにくいな…)
まず、カーネルの中に色々と待ちキューがある様子を想像…しなくていいか…ソースコード見ればわかる?
// futexの例 #define _GNU_SOURCE #include <linux/futex.h> #include <pthread.h> #include <unistd.h> #include <sys/syscall.h> int key; void *f(void*p) { sleep ( 2 ); puts("okiroyo-"); syscall( SYS_futex, &key, FUTEX_WAKE, 1 ); } int main() { pthread_t t; int v = key; pthread_create( &t, NULL, f, NULL ); syscall( SYS_futex, &key, FUTEX_WAIT, v, NULL ); puts("okitayo"); }
こんな感じにある変数をキーにして、待ったり起こしたりできる。
…と、いう非常に単純な同期プリミティブがfutex。このfutexの特徴を見てみる
上の操作だと、もし、万が一、FUTEX_WAITする前にFUTEX_WAKEしてしまうと、FUTEX_WAKEをとりこぼしてしまう。(永久にSYS_futexから帰ってこない)
// 彼は永久に帰ってこない… #define _GNU_SOURCE #include <linux/futex.h> #include <pthread.h> #include <unistd.h> #include <sys/syscall.h> int key; void *f(void*p) { puts("okiroyo-"); syscall( SYS_futex, &key, FUTEX_WAKE, 1 ); } int main() { pthread_t t; int v = key; pthread_create( &t, NULL, f, NULL ); sleep( 1 ); /* ここで何かすごいスケジューリングが! */ syscall( SYS_futex, &key, FUTEX_WAIT, v, NULL ); puts("okitayo"); }
こんな感じの問題を防ぐために、futexでは、引数の値と実際に変数が指してる値が違うと失敗する、というようになっている。
// futexが失敗する例 (EAGAIN) #define _GNU_SOURCE #include <linux/futex.h> #include <pthread.h> #include <unistd.h> #include <sys/syscall.h> #define WOKE 1 int key = 0; void *f(void*p) { puts("okiroyo-"); key = WOKE; syscall( SYS_futex, &key, FUTEX_WAKE, 1 ); } int main() { pthread_t t; int v = key; pthread_create( &t, NULL, f, NULL ); sleep( 1 ); /* ここで何かすごいスケジューリングが! */ syscall( SYS_futex, &key, FUTEX_WAIT, v, NULL ); puts("okitayo"); }
実際にはもう少しややこしいと思うのだけど…そこらへんは…えーと、どこだったかな…。どっかでmutexとcond variableを実装しているコードを見たんだけど…思い出せない。まあいいか、そこらへんは各自の宿題ということで。(宿題:スレッドがブロックしない場合システムコール呼び出ししないmutexを書きなさい。僕には無理です… futex(7)にちょっと書いてある)
Win32にあって、pthreadには無いもので、すぐに思い付くものに、「イベントオブジェクト」がある
イベントオブジェクトは、「シグナル状態」と「非シグナル状態」のふたつの状態のどちらかを保持する変数。SetEvent、ResetEventのふたつのAPIを使うことで、それぞれ、「シグナル状態」と「非シグナル状態」に変更できる
非シグナル状態のイベントオブジェクトに対してWaitFor〜すると、ブロック、シグナル状態のイベントオブジェクトに対してWaitForSingleObjectすると、ブロックしない。というもの。
pthreadの conditional variableは、状態を保持しないので、これとは違う。
(僕の)練習がてら、futexを使ってこの「イベントオブジェクトもどき」を実装してみる
#include <pthread.h> #include <unistd.h> #include <sys/syscall.h> #include <linux/futex.h> #include <errno.h> #define EVENTOBJ_RESET 0 #define EVENTOBJ_WAITANY 1 #define EVENTOBJ_SIGNALED 2 void reset_event( int *ev ) { *ev = EVENTOBJ_RESET; __sync_synchronize(); } void set_event( int *ev ) { int perv_val = *ev; *ev = EVENTOBJ_SIGNALED; if ( perv_val == EVENTOBJ_WAITANY ) { sleep ( 1 ); syscall( SYS_futex, ev, FUTEX_WAKE, 1024 ); } } void wait_for( int *ev ) { int v; retry: v = *ev; if ( v == EVENTOBJ_SIGNALED ) return; if ( !__sync_bool_compare_and_swap(ev,v,EVENTOBJ_WAITANY) ) goto retry; if ( syscall(SYS_futex,ev,FUTEX_WAIT,EVENTOBJ_WAITANY,NULL) == -1 ) { if ( errno == EAGAIN ) { goto retry; } } } void * signal_thread( void *a ) { while ( 1 ) { sleep(1); set_event( (int*)a ); } } int main() { int v; pthread_t t; reset_event( &v ); pthread_create( &t, NULL, signal_thread, &v ); wait_for( &v ); puts("r"); wait_for( &v ); puts("r2"); }
あってるかどうか自信が無いけど…
futexは、epollやselectで監視できるファイルディスクリプタを取得することができる
(これ、pthreadのmutexに比べて、ひっっじょおーに大きなアドバンテージだと思うのだけど、いかがか?)
別スレッドからの通知と、I/Oの完了を同時に待つことができるのである。(もうI/O監視スレッド作らなくていいんだよ!)
例えば、ユーザ入力とスレッドからの通知を待つのは以下のように書ける
#include <pthread.h> #include <linux/futex.h> #include <fcntl.h> #include <sys/epoll.h> /* Linux使いならもちろんepollだよね! */ #include <sys/syscall.h> #include <stdio.h> #include <unistd.h> int key; void * f( void * p) { sleep( 3 ); syscall( SYS_futex, &key, FUTEX_WAKE, 1 ); } #define P_STDIN 0 #define P_THREADIN 1 int main() { pthread_t t; int threadfd; int pollfd; struct epoll_event ev[2]; pthread_create( &t, NULL, f, NULL ); threadfd = syscall( SYS_futex, &key, FUTEX_FD, 0 ); pollfd = epoll_create( 2 ); ev[0].data.u32 = P_STDIN; ev[0].events = EPOLLIN ; epoll_ctl( pollfd, EPOLL_CTL_ADD, 0 /*stdin*/ , &ev[0] ); ev[1].data.u32 = P_THREADIN; ev[1].events = EPOLLIN ; epoll_ctl( pollfd, EPOLL_CTL_ADD, threadfd, &ev[1] ); { int nev = epoll_wait( pollfd, ev, 2, -1 ); int i; for ( i=0; i<nev; i++ ) { if ( ev[i].data.u32 == P_THREADIN ) { puts("from thread"); } else { char buf[512]; int n = read( 0, buf, 512 ); buf[n-1] = '\0'; /* '\n'->'\0' ?? EOF? */ printf("from user: '%s'\n", buf ); } } } }
(このへんまで書いてなんかちょっと違うような気がしてきた)
でもこの FUTEX_FD、もうすぐ無くなる予定なので使わないでね!(dmesgを見てみよう!)
--
<ヨタ話>。僕の中ではFUTEX_FDさえあれば全てが美しくなってヒャッホいだと思ってたのだけど、廃止されるのってどうなのかな…
ここで色々と妄想。futexをI/O通知の仕組みに近付けていくのではなくて、select、poll、epollの重要度を下げていって、I/Oの通知をfutexで行う、というようになる、というのはアリかも。例えば、aioとかで、(ここまで書いて、色々とあるようだと知ったので、どうでもよくなった)
僕があとで読む系