15.1 | スレッドの基本
シングルスレッドアプリケーションは、レジ係が 1 人しかいないスーパーのようなものです。スーパーの経営者からすれば、レジ係が 1 人なら人件費節約になりますが、応対できる客数が制限されます。このため、大安売りの日などはレジ前に長蛇の列ができ、一部の客から不満が出ることになるでしょう。これは一般的なボトルネックです。つまり、多量のデータが流れているのにもかかわらず、その流れの幅が狭すぎるのです。もちろん、レジ係を増やす以外に解決策はありません。
スーパーとレジ係の関係は、アプリケーションとスレッドの関係と同じです。アプリケーション内で複数のスレッドを使用すれば、処理を分割し、相互に独立して行うことができます。これは、プロセッサとユーザータイムを最も効率的に活用することであり、作業効率が改善されます。しかし、これまでマルチスレッドプログラミングの経験がない場合、注意が必要です。マルチスレッドは、すべてのアプリケーションにおいて必ずしも正しい選択肢ではありませんし、動作が遅くなってしまうこともあります。このため、マルチスレッドプログラミングを学習する際には、それをいつ使用すべきかを理解しておくことがきわめて重要です。本章では、このようなことも考慮し、最後に「15.5 スレッド操作指針」を設けてあります。自分のアプリケーション内でマルチスレッドを応用する際の参考にしてください。それでは、Microsoft .NET Framework に実装されているスレッド操作コードを、まず検討することにしましょう。
15.1.1 | スレッドとマルチタスク
スレッドは処理の単位であり、マルチタスクは複数のスレッドを同時に実行することを意味しています。マルチスレッドには、協調的なマルチスレッドとプリエンプティブなマルチスレッドの 2 種類の実装方法があります。Windows の初期バージョンは、協調的なマルチスレッド手法を採用していました。この手法は、制御を握っているそれぞれのスレッドは、自分の責任で制御権をプロセッサに返すように義務づけられていました。つまり、プロセッサはスレッドから制御権を返してもらってから、他のスレッドを実行していたわけです。
他の OS (私の場合なら、IBM System/38(CPF) と OS/2 )を使用した経験のある開発者は、おそらく、初期の Windows システムをハングさせてしまった経験も同時に持っていることでしょう。たとえば、PeekMessage 呼び出しをアプリケーション内に入れ忘れ、システム内の他のスレッドが CPU を使用できなくなってしまったことがあるはずです。どうして? というのが私たちの反応だったはずです。
しかし、Windows NT、Windows 95、Windows 98、そして Windows 2000 では、OS/2 と同じプレエンプティブなマルチタスク手法をサポートするようになっています。プリエンプティブなマルチタスク環境では、個々のスレッドに実行時間を与えるのはプロセッサの責任となります。このような実行時間を「タイムスライス」と呼んでいます。プロセッサは複数のスレッドを切り替え、それぞれにタイムスライスを与えます。プログラマは、このような事情を知る必要はありません。つまり、スレッドは、他のスレッドが動作を開始できるように、現在持っている制御権をいつ放棄するかなどを考える必要がありません。これから取り上げる .NET は、プリエンプティブな OS 上でのみ動作します。
ところで、1 個のプロセッサを搭載したマシンでアプリケーションを実行している場合、OS がいくらプリエンプティブなマルチタスクをサポートしているとしても、実際には、複数のスレッドが同時に実行されることはありません。プロセッサは、一定の間隔(ミリ秒単位)でプロセスを切り替えているため、そのように見えてしまうだけです。同時に複数のスレッドを実行させたい場合には、複数のプロセッサを搭載しているマシン上でプログラムを開発し、そこで実行する必要があります。
15.1.2 | コンテキスト切り替え
コンテキスト切り替えはスレッドの本質ですが、かなり難しい概念です。そこで、ここでは簡単に概要だけを説明しておきましょう。
プロセッサは、ハードウェアタイマを使って、スレッドに割り当てられたタイムスライスの時間切れを決定しています。ハードウェアタイマから割り込み通知を受け取ると、プロセッサは現在のスレッドのすべてのレジスタ値をスタックに退避します。その後、プロセッサは、スタックからこれらのスタック値を CONTEXT 構造体と呼ばれるデータ構造体に移動します。プロセッサが以前のスレッドに再度タイムスライスを割り当てるとき、これまでの退避手順を逆順に繰り返し、スレッドに関連する CONTEXT 構造体からレジスタ値を回復します。このようなレジスタ値の退避と回復が、コンテキスト切り替えと呼ばれます。