Javaはなぜ非同期処理を行えるか

また例によって考えてみた.僕らJ2EE技術者は「Log4jとかって非同期でログを出してくれているんだよね」とか「非同期処理をしたかったらThreadをnewして別スレッドで処理を実行させろ」とか「でもEJBではThread使うの禁止だからMessage Driven Bean使え」とか思っている.

そんなの当たり前だと昨日の今日まで思っていたわけだけど,本当はなんで非同期処理ができるのか,よ〜く考えると,まるで分かっちゃいない.

「いやだって別スレッドで起動すれば非同期に」とここで言うと無限ループ.

さて,実は数日前『詳説Linuxカーネル』という本を買って,そういったことを理解するための情報源を入手していたので調べてみた.(この本はOSの一般的なアーキテクチャを理解するための教科書として秀逸,との評判があったので)

ちょこっと引用抜粋.まずはプロセスから.

プロセスの概念は,どのようなオペレーティングシステムでも基本となる考え方です.通常プロセスは,プログラムの実行時におけるインスタンスとして定義されています.

カーネルから見ると,プロセスの目的は,システム資源(CPU時間やメモリなど)を割り当てて実体として動かすことです.

プロセス生成時,そのプロセスは親プロセスとほとんど同じです.親プロセスのアドレス空間の複製を引き継ぎ,親プロセスと同じ命令を実行します.プロセス生成するシステムコールの次の命令から実行を始めます.親プロセスと子プロセスはプログラムコード(テキスト域)の載ったページを共有しますが,データ領域(スタック域とヒープ域)は別々の複製を持ちます.そのため,子プロセスのメモリ内容の変更は,親プロセスからはわかりません.

要するにプロセスはカーネルによってCPU時間とメモリをそれぞれ独立して割り当てられる"インスタンス"ということ.(この分野でインスタンスという言葉が出てきたのには驚いた.)

そして,プロセス間ではCPU時間とメモリは排他的に与えられて動作するということ.

つまり(ソケットや共有メモリを使わないならば),プロセス間でデータ(メモリ)の共有はできないということ.

ここで1つ問題が出てきたらしい.

古いバージョンのLinuxカーネルは,マルチスレッドアプリケーションのための機能を持ちませんでした.カーネルから見ると,マルチスレッドアプリケーションは通常のプロセスでした.マルチスレッドにおける複数の処理の流れを生成・操作・スケジュールすることは,すべてユーザモードにおいて実現されていました.

しかし,このようなマルチスレッドアプリケーションの実装は,好ましいものではありません.例えば,チェスのプログラムが2つのスレッドを使うとします.1つのスレッドはチェス盤の表示を担当します.人間のプレイヤーが駒を動かすのを待ち,コンピュータが動かした駒を表示します.その間,もう1つのプロセスはゲームの次の手を熟考します.1つ目のスレッドが人間が動かす駒を動かすのを待つ間,2つ目のスレッドはずっと走り続けているべきです.人間のプレイヤーが考えている時間を利用できます.しかし,チェスプロセスが単なるプロセスのときは,1つ目のスレッドはユーザの操作を待つために,簡単にブロックするシステムコールを発行するわけにはいきません.さもなくば,2つ目のプロセスもブロックされてしまいます.代わりに,1つ目のスレッドは賢いノンブロッキング手法を利用し,プロセスの走行可能状態を維持しなければなりません.

つまり,擬似的にスレッドを実装したアプリケーション(1つのプロセスとして実行)では,実行状態の管理を柔軟に行うことができなかった.

そこで,スレッドがLinuxカーネルに実装される.

Linuxは,マルチスレッドアプリケーションのためのより優れた機能として,ライトウェイトプロセスを提供します.基本的に,2つのライトウェイトプロセスはいくつかの資源を共有します.アドレス空間やオープンしているファイルなどの資源です.一方が共有資源を変更すると,他方のライトウェイトプロセスからもすぐその変更がわかります.もちろん,2つのライトウェイトプロセスは共有資源のアクセス時に排他することを忘れてはなりません.

ライトウェイトプロセスが利用可能であれば,マルチスレッドアプリケーションの素直な実装として,ライトウェイトプロセスを各スレッドに割り当てます.この方法では,各スレッドが同じメモリアドレス空間・同じオープンファイルなどを共有することにより,アプリケーションの同じデータを参照できます.また同時に,カーネルは書くスレッドを独立してスケジューリングし,あるスレッドが動作中に別のスレッドが待ちに入ることができます.

つまり,メモリ空間とI/Oの共有をしつつ,それぞれ独立したCPU時間を与えられて動作するようになったのがスレッド(ライトウェイトプロセス).

Javaはマルチスレッドが言語自体に組み込まれている」といった感じの表現をこれまで良く見てきたので,てっきり僕はJVMがマルチスレッドの仕組みを実現しているのだと思い込んでいたんだけど,ほんとうは全部OSがやっていた「スレッドに共有リソースと独立CPU時間を与える」という機能のお陰だったわけだ.

えぇ,本当?と思うかもしれない.というか自分も少しはそう思ったのでちょっと調べてみた.

Javaがマルチスレッド機能を"実装"していない証拠(ラッパーにすぎない証拠)】

証拠といいつつ,大したもんではないのだが,まずJ2SE1.4.2のソースを覗いてみた."java.lang.Object"クラスと"java.lang.Thread"クラスね.

wait(), sleep(), start()等のスレッド系APIが native メソッドであることを確認.でもまだこれくらいじゃ納得しない方もいるでしょう(というか自分).

さらに,"java.lang.Object"クラスと"java.lang.Thread"クラスのクラスファイルを逆アセンブルしてどんなバイトコードになっているのかを覗いて見た(javap).

すると

public synchronized native void start();

….nativeメソッドって,バイトコードでも"native"なんだ(笑

# というか,よく考えればその通りなんだが.

証拠としてはこれだけなんだが,もうちょい理由付けを.

上の引用の例にもあるように,プロセスが擬似的にスレッドを実装しようとすると,結局タイムシェアリングのところで問題が発生することになる.WEBアプリケーション(特にWEBコンテナ)等,マルチスレッドを多用するJavaが,そんな下手なやり方をするわけがない.カーネルに処理をお任せするのが適切なアーキテクチャだろう.

そんなところでどうだろうか.(自分的には納得.)

かくして,カーネルが"実装"しているスレッド機能をJREがラップすることで,Javaではスレッドを使うことが出来ているというわけ.でもって,掲題に戻るけど,非同期処理が出来るというわけ.

                                                                                                                      • -

さて,以上の話は,本当のところ合っているのだろうか.

Linux版のJVMの内部では,pthreadライブラリのpthread_create関数とかを呼び出しているのだろうか?

それとももうちょい低レベルのシステムコールか何かの仕組みを上手くつかっているのだろうか?

もしご存知の方,上記記述に誤りを発見した方は,お気軽にコメントいただければ…幸いです.