マルチスレッドのプログラミング

入出力の問題

マルチスレッドプログラミングの利点の 1 つは、入出力の性能を高めることができることです。従来の UNIX の API では、この点に関してほとんどサポートされていませんでした。つまり、ファイルシステムに用意されている機構を利用するか、ファイルシステムを完全にバイパスするかのどちらかの選択肢しかありませんでした。

この節では、入出力の並行化やマルチバッファーによって入出力の柔軟性を向上させるためのスレッドの使用方法を説明します。また、同期入出力 (スレッドを使用) と非同期入出力 (スレッドを使用することも使用しないこともある) の相違点と類似点についても説明します。

遠隔手続き呼び出しとしての入出力

従来の UNIX のモデルでは、あたかも入出力装置に対して遠隔手続き呼び出しを行なっているかのように、入出力が同期した状態に見えました。呼び出しが復帰した時点では、入出力は完了しているか、少なくとも完了しているように見えます。たとえば、書き込み要求は、オペレーティング環境内のバッファーにデータを転送するだけで終了することがあります。

このモデルの利点は、よく使われる手続き呼び出しの考え方が利用されている点です。

従来の UNIX システムにはなかった代替モデルに非同期モデルがあります。このモデルでは、入出力要求は操作を開始させるだけです。プログラム側がなんらかの方法で操作の完了を検出しなければなりません。

非同期モデルは同期モデルほど簡単ではありません。しかし、従来のシングルスレッドの UNIX プロセスでも並行入出力などの処理が可能であるという利点があります。

非同期性の管理

非同期入出力のほとんどの機能は、マルチスレッドプログラムによる同期入出力で実現できます。非同期入出力では、要求の発行後、入出力がいつ完了したかをチェックします。同期入出力では、独立したスレッドで入出力を同期的に実行できます。メインスレッド側は、pthread_join(3C) の呼び出しなどによって、あとで入出力操作の完了を確認できます。

非同期入出力

各スレッドの同期入出力で同じ効果を実現できるため、非同期入出力が必要になることはほとんどありません。ただし、スレッドで実現できない非同期入出力機能もあります。

簡単な例は、ストリームとしてテープドライブへ書き込みを行う場合です。この場合、テープに書き込まれている間は、テープドライブを停止させないようにします。テープに書き込むデータをストリームとして送っている間は、テープは高速で先送りされます。

ストリーミングをサポートするには、カーネル内のテープドライバはスレッドを使用する必要があります。カーネル内のテープドライバは、割り込みに応答するときに、待ち行列に入っている書き込み要求を発行する必要があります。この割り込みは、以前のテープへの書き込み操作が完了したことを示すものです。

スレッドでは、書き込み順序を保証できません。スレッドの実行される順序が不定だからです。たとえば、テープへの書き込み順序を指定することはできません。

非同期入出力操作

#include <aio.h>

int aio_read(struct aiocb *aiocbp);

int aio_write(struct aiocb *aiocbp);

int aio_error(const struct aiocb *aiocbp);

ssize_t aio_return(struct aiocb *aiocbp);

int aio_suspend(struct aiocb *list[], int nent,
    const struct timespec *timeout);

int aio_waitn(struct aiocb *list[], uint_t nent, uint_t *nwait,
    const struct timespec *timeout);

int aio_cancel(int fildes, struct aiocb *aiocbp);

aio_read(3RT)aio_write(3RT) は、概念において pread(2)pwrite(2) に似ています。ただし、入出力操作のパラメータが、aio_read() または aio_write() に渡される非同期入出力制御ブロック (aiocbp) に格納される点が異なります。

    aiocbp->aio_fildes;    /* file descriptor */
    aiocbp->aio_buf;       /* buffer */
    aiocbp->aio_nbytes;    /* I/O request size */
    aiocbp->aio_offset;    /* file offset */

さらに、必要に応じて、「struct sigevent」のメンバーで非同期通知タイプ (一般には、待ち行列に入れられたシグナル) を指定することができます。

    aiocbp->aio_sigevent;  /* notification type */

aio_read() または aio_write() を呼び出すと、入出力操作が開始されます (または、入出力要求が待ち行列に入れられます)。この呼び出しは、ブロックされずに復帰します。

非同期操作の進行中のエラー状態や復帰状態を判定するために、aiocbp の値を aio_error(3RT) および aio_return(3RT) の引数として使用できます。

入出力操作の完了の待機

1 つ以上の未処理の非同期入出力操作の完了を、aio_suspend() または aio_waitn() を呼び出すことによって待機することができます。入出力操作の成功または失敗を判定するには、完了した非同期入出力制御ブロックに対して aio_error() および aio_return() を使用します。

aio_suspend() および aio_waitn() 関数には、呼び出し側の待ち時間を示す timeout 引数があります。NULL ポインタを指定すれば、呼び出し側が無期限に待つという意味になります。値 0 が設定されている構造体を指すポインタは、呼び出し側がまったく待たないという意味になります。

非同期入出力操作を開始し別の処理を行なって aio_suspend() または aio_waitn() で操作の完了を待つことができます。あるいは、aio_sigevent() で指定された非同期通知イベントを発生させて操作の完了を通知するという方法もあります。

最後に、保留状態の非同期入出力操作を取り消すときは、aio_cancel() を呼び出します。この関数を呼び出すときは、入出力操作を開始するために使用された入出力制御ブロックのアドレスを指定します。

共有入出力と新しい入出力システムコール

複数のスレッドが同じファイル記述子を使って同時に入出力操作を行う場合、従来の UNIX の入出力インタフェースがスレッドに対して安全ではない場合があります。この問題は、lseek(2) システムコールがファイルオフセットを設定するとき、入出力が逐次的に行われない場合に発生します。ファイルオフセットは、次の read(2) または write(2) 呼び出しでファイル内の操作開始位置を指定するときに使用されます。このとき、同じファイル記述子に対して複数のスレッドが lseek() を実行してしまうと矛盾が生じます。

この問題を回避するには、システムコール pread()pwrite() を使用します。

#include <sys/types.h>
#include <unistd.h>

ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset);

ssize_t pwrite(int filedes, void *buf, size_t nbyte,
    off_t offset);

pread(2)pwrite(2) は、read(2)write(2) と同様に機能しますが、ファイルオフセットの引数が追加されている点が異なります。lseek(2) の代わりに、この引数でオフセットを指定すれば、複数のスレッドから同じファイル記述子に対して安全に入出力操作を実行できます。

getcputc の代替

標準入出力に関して、もう 1 つ問題があります。getc(3C)putc(3C) などのルーチンは、マクロとして実装されていて非常に高速に動作するという理由でよく使用されています。getc(3C)putc(3C) が高速であるため、これらのマクロは、プログラムのループ内でも効率を気にせずに使用できます。

しかし、getc(3C)putc(3C) は、スレッドに対して安全になるように変更されたため、以前よりも負荷が大きくなっています。これらのマクロは、相互排他のロックと解除のために、少なくとも 2 つの内部サブルーチンの呼び出しを必要とします。

この問題を回避するため、代替マクロとして getc_unlocked(3C) および putc_unlocked(3C) が用意されています。

getc_unlocked(3C)putc_unlocked(3C) は、相互排他ロックを獲得しません。これらの getc_unlocked() または putc_unlocked() マクロは、スレッドに対して安全ではない元の getc(3C) および putc(3C) と同程度の処理速度を実現しています。

しかし、これらのマクロをスレッドに対して安全な方法で使うためには、flockfile(3C) funlockfile(3C) を使って、標準入出力ストリームを保護する mutex を明示的にロックし、解放する必要があります。これらのルーチンは、ループの外で呼び出します。getc_unlocked()putc_unlocked() はループ内で呼び出します。