Linuxではじめる快適スレッドライフ

概要

pthreadはクソだというのをひしひしと実感している…わけではないけど、 Win32のスレッドと比べると微妙な感じが拭えないので、Linuxのスレッドを使えばもっとハッピーになれるよ、というような話。

pthreadがクソな理由

pthreadは Win32 のスレッドに比べていくつか微妙だと思われる点がある。まずはその点について書こう、と思ったのだけど、大体[ここ]に書いてあったので、列挙だけしておく。詳細はリンク先を読んでね!

他にもあるかも。timedwaitの時間がキモいとか。

Linuxスレッド

と、そういうわけでなんだかLinuxがWin32に劣るみたいな感じになりそうなので、ここで、LinuxのスレッドがWin32スレッドに劣っていない、というような話を書いておく。

(標準に準拠しないAPIを使うデメリットは考えない)

futex、signalfdの順に見ていこう。

-- 超雑談。Windowsは何故かVistaになってから条件変数を用意[これ] --

futex

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 CreateEventもどき

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_FD

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とかで、(ここまで書いて、色々とあるようだと知ったので、どうでもよくなった)

リンク集

僕があとで読む系