Linux + スレッド のススメ ごとむ (Copyright (C) 1999, 2000 GOTO Masanori) かなり前から目にすることであったが、Linux における Pthread、ひいては thread に関する情報や文章がほとんど提供されていないことで、 あちこちのメーリングリストやニュースグループ、そして Web で十分な 情報がなく、質問が来ても答えがない、また誤った解釈がそのままに なっている例をよく見掛けることが気になっていた。 また多くの場合、個々の情報は提供されているものの、 Linux における全体的な話はほとんど目にしない。 なお、知っている人にとっては読んでも面白おかしくないだろうし、 知らない人にとってもどうでもいいことだろうし、 調べたい人は、おそらく別の文献に既に当たるだろうと思われる。 つまり書く意味がどれほどあるかは大変疑問である :-)。 といってしまえば身も蓋もないのだけど、 今回書かないと IRC から ban されるらしいし (笑)、 折角の機会でもあるし、文章としてまとめることにした (丁度まとめたかったので利用することにしただけだったり :-)。 なおあらかじめ断っておくが、文中で技術的に誤っている 部分があるかもしれない。また分進秒歩のこの世界、 古い記述が混ざっているかもしれない。 その時は是非教えて頂けると嬉しい限りである :) ● スレッドとは何か スレッド (thread) が一般的になったのは最近のことである。 そこでまず始めに、簡単にスレッドについて紹介したい。 簡単に言ってしまえば、スレッドとは Un*x のプロセス (process) という概念に かなり似たものであり、プロセスの制御を更に個々にわけたものであると言える。 プロセスベースのモデルでは、プロセス中で単一の制御の流れだけがある。 通常のアプリケーションではおそらく常識的なことである。 しかしその流れを一本から、複数のタスクにわけて動作させようとすると、 これまでは各プロセスを fork() するなどしてその仕事の数だけ 分割し、それぞれをシグナルやセマフォ、スケジューリング等を用いて 動かす方法しか提供されていなかった。 しかし実際のプログラムでは、同時にいくつもの並行作業を行いたいと 思うことがある。そう、一つのプロセスの中で複数の制御を走ることが 出来ればよいのである。このプロセス中のそれぞれの制御をスレッドと呼ぶ。 こうすることで、次の利点がある: * スレッドごとにメモリを共有可能 作業をプロセス毎に分割しないため、プロセス毎に 管理されているメモリ領域 (具体的にはプログラムのコンテキスト、 レジスタ、スタック、ヒープなど) やファイルディスクリプタなどを、 そのプロセスに属するスレッド全てで共有することが可能となる。 スレッド無しにプロセスだけで処理しようとすると、何らかの形で プロセス間通信 (IPC: Inter Process Communication) しなければ ならないため、色々なテクニックを用いなければならない。 * コンテキストスイッチの負荷軽減 また一般的にマルチタスクな OS では、一定時間毎にプロセスを切り替える (コンテキストスイッチ) ことで擬似的に複数の仕事が並列実行しているよう に見せているのだが、このスイッチにかかるコスト (例えばレジスタやスタックの待避) はそれだけでもかなり大きい。 これを少しでも軽減することが可能になれば、その分だけ CPU を計算時間 等にまわすことができる。 * マルチプロセッサに処理を分散可能 例えば 1 つのプロセスだけで動作するプログラムの場合、 CPU を 1 つだけ占有しておけば良かった。がしかし、 しかしマルチプロセッサシステムの場合、折角 CPU が いくつもあるのに、プログラムは 1 つのプロセスのみで 動いているので、CPU も 1 つしか使うことができなくなる。 しかしスレッドを使えば、プロセッサ毎にプログラムの 動作を分散させることが可能になる。 もちろんマルチプロセッサになった場合、プロセスを 1 つだけから複数使うように変更すればいいだけだが、 大抵はそのようなことを考えてプログラムされていない。 もちろんこれは実装にもよるが、現在の一般的な Un*x では、スレッドを使うことが手頃かつ 簡単にマルチプロセッサに処理を分散することが 可能になる解法の 1 つであると言える。 しかし利点もあれば、欠点もある。 * 変数の値を予想外に変更される危険性 各スレッドはメモリを共有しているため、例えばグローバル変数も 各スレッドで共有されている。これはこれで便利なのだが、反対に、 各スレッドがバラバラに値を格納したがために プログラマが意図しない結果になってしまうかもしれない危険性がある。 もちろんその辺りのことはちゃんと考えられていて、 mutex と呼ばれるものを用いて変数を 保護したり、スレッド間で連携させるようにすることが可能である。 ただしプログラマはこの辺りを明示的に 設計し、プログラムしなければならない。 * スレッドセーフ (thread safe) なライブラリが必要 これまで使われてきたライブラリはスレッドを考慮に 入れた設計がされていない。そのためスレッドに対応していない 関数があるため、対応するように書き換えたライブラリを 持つ必要がある。 ちなみにスレッドに対応しているライブラリを スレッドセーフなライブラリと呼ぶ。 * シグナル (signal) など、これまでの手法に制限が生じる スレッドを用いないシステムの場合、シグナルは 単純に送りたいプロセスに向かって送信すれば 良かった。しかしスレッドを用いたシステムの場合、 どのスレッドがシグナルを受け取るのか、などについて 作り込まなければならない。 * デバッグが困難 スレッドを使用することでぶちあたる問題の一つが、 デバッグが非常に困難であるということである。 単一のプロセスの場合は、単純にどのコードが実行されているか その時点その時点ではっきりしているので、それを追っていけば良い。 しかしスレッドを使っている場合、複数のスレッドが、 同じプロセス内で様々にデータを書き換えながら動くため、 デバッガもスレッドに対応しているものを使わなければならない。 最近の gdb ではマルチスレッドに対応しつつあるものの、 現時点では部分的に対応されてはいるものの、完全では ない。そのためデバッガを使ったマルチスレッドのデバッグ を行うと、個々のスレッドが何を行っているか完全に 把握することができないため、デバッキングには相当の困難が伴う。 今後マルチスレッドに完全対応した gdb がリリースされれば この辺りの事情はもっと改善することが期待される。 こういった利点・欠点をうまく使い分けつつ 利用できるところは利用することがポイントだろう。 ● Pthread とは これまでは単純にスレッド、とひとくくりにしてきたが、 実際に利用するためのインターフェースやライブラリには色々と種類がある。 例えば、Solaris UI スレッド、DCE スレッド、Pthread など。 その中で Pthread とは、正式名称を POSIX 1003.1c-1995 (または ISO/IEC 9945-1:1996) という、規格や実装を指す通称であり、 特に Un*x では、数あるスレッド規格の中でも、 最も汎用的で利用可能なものとして Pthread が使われている。 これは IEEE の POSIX や OpenGroup の Unix98 等 「標準」的な規格書に規格化されているためでもある。 そのほかのスレッド規格は、大抵が独自路線であったり、 特定 OS やシステムに依存している場合が多いため、 汎用的でないことも Pthread が利用される一因であろう。 そのため Pthread をサポートするシステムならばどれでも 同じ C のソースコードで作動するため移植が非常に容易である。 また規格がきちんと規格書に記述されているため、 規格変更といったことを気にする事なく使用することができる。 ● 実際に使ってみる というところで、まずは早速使ってみよう。 次のプログラム 1 を打ち込んでセーブし、 コンパイルして実行してみよう。 次のような結果が得られたはずである (ただしコンパイルと実行を行うシステムは Pthread を サポートしていることが必須条件である)。 ----------- コンパイルと実行結果 % gcc -o p1 p1.c -lpthread % ./p1 ----------- この文章は Pthread プログラミング解説書ではないので、 また紙面と時間の関係もあるため、これ以上は省略する。 もっとスレッドプログラミングしてみたくなったら、 何か適当な本を読んでみると面白いと思う。 ----------- プログラム 1: p1.c ----------- ● カーネルにおけるスレッドの表現方式 これまでスレッドのインターフェースには、主に Pthread と 呼ばれるものがあることは紹介した。 Pthread ライブラリを利用する限り、普通のプログラムで システム毎の差異を気にする必要はない。 だが Pthread はあくまでもライブラリであり、どう実装するかは ベンダや作成者の自由にまかされている。 そのため、実装によってプログラムの速度などが変わってくる。 実際にシステム上でスレッドを実現するときには、 各システムの事情なども背景になっていることもあって、 様々なモデルや構成の仕方があり複雑であるので、 一言で言うのは難しい。しかし、 しかし大まかにわけて、カーネルにおけるスレッドの実体 (kernel entity) ** がどのように表現されるかという点から、 主に以下の3つの方式に分類することが出来る。 * ユーザーレベル (user level) 別名、多対一 (many-to-one) 方式とも呼ばれる。 各プロセス中において、ユーザーが全てのスレッドを制御スケジューリング する方法。カーネルはスレッドが属するプロセスしか認識しない。 * カーネルレベル (kernel level) 別名、一対一 (one-to-one) 方式とも呼ばれる。 ユーザーレベルと違い、カーネルが各スレッドを 制御・スケジューリングするものである。 * 2レベル (2 level) 別名、多対多 (many-to-many) 方式、または mixed スレッドとも呼ばれる。 ユーザーレベルとカーネルレベルを組み合わせたもの。 これら実装方式の違いによって、次のような利点欠点が生まれてくる。 利点 欠点 ユーザレベル * スレッド切り替えが高速 * あるスレッドがブロックすると * ライブラリが OS に依存しない スレッド全体がブロックされる ため移植性が良い * マルチプロセッサを利用できない カーネルレベル * スレッドがブロックされても他 * スレッドの切り替えが重くなる スレッドもブロックされない * スケーリングが悪い * マルチプロセッサに対応 * ライブラリが OS に依存する 2 レベル * スレッドがブロックされても他 * ユーザが直接的な制御が難しくなる スレッドもブロックされない * 実装が複雑 * マルチプロセッサ対応 * ライブラリが OS に依存する * スケーラビリティが比較的良い * スレッドの切り替えが少し重くなる * ユーザレベル・カーネルレベルを組み合わせられる どれが良いか悪いかは、それぞれのアプリケーションに依存する。 例えばネットワークアプリケーションをスレッド化するならば、 ユーザーレベルよりもカーネルレベルが良いだろうし、 頻繁にコンテキストスイッチを繰り返すなら、ユーザーレベルが良いだろう。 しかし 2 レベルは色々対応している点で魅力的である。 ● Linux カーネルのスレッド対応 では、Linux ではどのようにスレッドに対応しているのだろうか。 まず最初にカーネル側での対応状況について、続いてライブラリ側 について触れたい。 それではまず Linux カーネルがスレッドにどう対応してきたかの 歴史について触れよう。 ○ カーネルのスレッド対応の歴史 カーネル 1.2 までは、そもそもスレッドは一切考慮されておらず、 カーネル側で特別なスレッドサポートを行っていなかった。 そのためこの頃のバージョンを使っている場合、 カーネルレベルスレッドを使用することが出来ず、 ユーザーレベルスレッドのみ使用可能である。 当然ユーザーレベルスレッドで実現しているライブラリ を利用しなければならない。また逆にカーネルスレッドを 利用したライブラリ (glibc の linuxthreads) などは 使用することは出来ない。 しかしカーネル 2.0 からは、__clone() システムコール をサポートすることによって、カーネルレベルスレッドが 利用可能になった。 更にカーネル 2.2 からは、スレッド用の signal が 新たに用意され、pthread_cond_* などを実装するときに signal を利用することが可能となった。 現在執筆時点の最新バージョンである 2.3 系でも カーネルの関数をスレッド対応する作業が続いている。 例えば SYSV 共有メモリのスレッド対応等である。 まだカーネルが完全にスレッド対応しているわけでは ないかもしれないため、今後もスレッド対応の 作業は続けられるであろう。 ○ __clone() システムコールとは さて、__clone(2) システムコール (sched.h) によって、カーネルスレッドが サポートされていると述べたが、これは一体なんだろうか? 要約すると、fork(2) と同様に子プロセスを作成する関数である。 ただし子プロセスのメモリ空間、ファイルディスクリプタ、シグナルハンドラ テーブルといったコンテキストのどれを親プロセスと共有するかを 指定できるという特徴を持っている。 ------------------------------- __clone() のプロトタイプ: int __clone(int (*fn) (void *arg), void *child_stack, int flags, void *arg) ------------------------------- 実際に、この __clone() を利用して作成された Pthread ライブラリである linuxthreads (glibc版) のソースコードには、以下のような記述を見つけることができる。 ------------------------------- /* Do the cloning */ pid = __clone(pthread_start_thread, (void **) new_thread, CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | __pthread_sig_cancel, new_thread); ------------------------------- __clone() 関数を使えば、子プロセスを作成するのと 似たような感覚でスレッドを生成することが可能になる。 しかし実際にユーザーがプログラムを組むときに、この __clone() システムコールを使うことはあまりないだろう。 というのも、このシステムコールは Linux に固有であり、 べったりと Linux カーネルに依存したプログラムに なってしまうし、Pthread の便利な機能も自分で作成しな ければならない。 そこで一般的にはライブラリがこの __clone() を隠してくれる。 そのライブラリを使えば、ユーザーがこのシステムコールを わざわざ叩くことなくスレッドを利用することが可能になっている。 逆に言えば、自分で Linux におけるカーネルスレッドを サポートしたライブラリやプログラムを独自に作成したい時は このシステムコールを利用すれば良いとも言える。 ○ Linux と 2 レベル さて、前節で述べたように、Linux カーネルでは カーネルレベルのスレッドが実装されていることに触れた。 まだ、後節でユーザーレベルのスレッドが Linux でも いくつか利用可能である。 しかし、商用 Un*x の多くに用意されており、また 様々な利点が多いはずの 2 レベル方式のスレッドは まだ実装されていない。なぜだろうか? これは実は Linux の作者である Linus Torvalds 氏や 開発者達が 2 レベルよりもカーネルレベルを推しているためである。 この理由は glibc 2.1.2 LinuxThreads 付属 FAQ K.2 に見つけることができる: ------------- K.2: Have you considered other implementation models? Linus Torvalds and other Linux kernel developers have always been pushing the "one-to-one" model in the name of overall simplicity, and are doing a pretty good job of making kernel-level context switches between threads efficient. LinuxThreads is just following the general direction they set. ------------- つまり簡単化のために、実装されていない、ということのようである。 残念ながら Linux で 2 レベルは、まだしばらく利用できそうにない。 ● Linux における Pthread ライブラリ ユーザーランドのアプリケーションをプログラムするときは、 当然スレッドに対応したライブラリを用いるのが普通だろう。 ここでは Linux で現在最も一般的に利用されていると思われる LinuxThreads から取り上げる。 ○ linuxthreads Linux では Pthread ライブラリを用いれば良い。 linuxthreads について linuxthreads とは、フランスの linuxthreads 0.8 glibc 付属 linuxthreads ただしこれは前述の通り、カーネルスレッドに対応した 実装になっているため、カーネル 2.0 以上でないと利用できない。 また、glibc などを使っていない古い libc5 などの システムでは libc5 用 LinuxThreads を使った方が良い。 ただしもちろんユーザーレベルのみをサポートしており、 また開発がほとんど行われていないため、内容が古いことにも 気をつけなければならない。 最後に Linux で glibc 版 LinuxThreads によって、 カーネルスレッドを使うときに心にとめておくべきことを あげておこう。 * 出来るだけ最新を使うべき ライブラリもカーネルもどちらも出来るだけ最新のバージョンを 使った方が良い。LinuxThreads や Linux、そして glibc は現在も 活発に更新され、バグフィックスされている。 最新のバージョンを出来るだけ利用することをおすすめする。 * 未実装機能の存在 まだ未実装の機能や関数がいくつかあり、利用不可能な ものがいくつか存在している。そのために利用や実現が可能でない 機能があるかもしれない。その場合は別の方法を用いた方が良いだろう。 * スレッドを無理して使う必要はない これは「ススメ」という題名とは全く正反対のことを言って しまっている :-) が、別に無理してスレッドを使わなければ ならない、ということはない。 もっと別の単純な方法や古くからある機能でプログラム化可能であれば、 移植性も向上するだろう。 もちろんスレッドを使った方が、プログラムが簡単になったり 見易くなったり、性能が向上することも多い。そういうときこそ スレッドの出番であろう。使った方が有効なときは 使うべきでせう :-) ○ その他の Pthread ライブラリ Linux で利用可能なスレッドライブラリは何も LinuxThreads に限らない。 ただし LinuxThreads 以外にカーネルスレッドを 実現している Pthread ライブラリはないようであるため、 全てユーザーレベルのものである。 * PTL http:// にある通り、Pthread をサポートしたライブラリ。 様々な OS で動作確認されている。 なお筆者は現在 .deb パッケージ作成に挑戦中であるため、 Debian で利用できる日も近い(はず) :-) * MIT 昔は Slackware などにも収録されており、libc5 システム には入っていることも多いと思う。 主要な関数は一通りあるものの、 マイナーな関数についてはほとんどサポートされておらず、 またどれくらい標準に準拠しているか不明である。 …最近どうなったかご存じの方いますか? ● その他のスレッド ○ 他の OS で使用できるスレッド 他の OS で扱えるスレッドを参考に書いてみた。 調べた限りでは、次の表 ** にあげる対応がされているようだ。 OS 名 ベンダ名 サポートするスレッド OS Interface スケジューリング方式 Linux 2.x - Pthread, clone() Kernel Solaris 5.7 Sun Pthread, Solaris UI Thread 2-level HP-UX 11.x HP PThread, DCEThread (CMA Library?) Kernel IRIX 6.2 SGI PThread (sproc interface) 2-level Tru64 4.0 Compaq PThread, MachThread? 2-level OpenVMS 7.0 Compaq PThread? user/kernel/2-level AIX 4.2 IBM PThread kernel WindowsNT Win32 Microsoft DCE? kernel? ○ C 言語以外でスレッドを利用できる言語 ここまでは、C 言語での利用を主な対象にしてきたが、 それ以外の言語でもスレッドを利用できるように 作られていたり、独自のスレッドライブラリを 用意しているものがある。それを幾つか挙げておく。 * ruby http://www.ruby-lang.org/ OO 指向言語 ruby にもユーザーレベルであるが、スレッドが実装されている。 比較的簡単に使える。 * Java http://www.blackdown.org/ スレッドを一番使っている言語ではないだろうか? と思う位に 利用されている。Java の本にはほとんどスレッドが解説されている。 Linux で使える Java に blackdown があるが、JDK 1.1.7 から Linux のネイティブスレッド (native thread) に対応している。 * その他、並列言語など ● 最後に 今回文章を書くに当たって、取りあえず時間がほとんど なかったこともあり、不十分な個所も多く見られるかもしれない。 技術的な部分は是非本や関連サイトを読まれて欲しい。 この文章を鵜呑みにした結果、説教を受けたり、 クラックされたり、半ズボンを穿き続けなければならなくなったとしても 筆者は何ら関知するところではない :-) 全体的に書き足りないところが多いと思うが、 Linux におけるスレッド情報をまとめることが 出来た点では良かったと思っている。 最近規格が整備されつつあり、熱くなってきた (ように思う) Real Time 処理との絡みについて の記述を加えることが出来なかったのは残念だった。 といっても、さっぱり分かっていないので、 書けなかっただけだったりするが :-) それでは最後に David R. Butenhof ** の言葉を借りて: Happy Threading! -------------- * POSIX: Portable ... IEEE (米国電子..) が作成している オペレーティングシステムの共通仕様書の一つ。 主に Un*x を念頭に置いて作成されている。 * Unix98: OpenGroup が作成している オペレーティングシステムの共通仕様書の一つ。 Unix98 は別名 SUSv2 (Single Unix Specification version 2) と呼ばれる。その中で Pthread に関係する規格名として XSH5 という 名称が使われることがある。 ** kernel entity とは Pthread のスレッド表現とプロセッサとの間での 実際の表現実体を意味する (らしい)。 ** なお、この表は http://www.serpentine.com/~bos/threads-faq/#19-What-operating-systems-provide-threads- を元に、筆者が独自に再構成したものである。 ** PThread の規格制定者の一人。「POSIX Thread Programming」という 本が著名。 ちなみにこれの邦訳はとても酷い。というか日本語になっていない。 買わないことをお勧めします :-(