Last Updated 2011/09/21
Microsoft は伝統的に用語の統一に熱心ではありません。一般的なコンピュータ用語と Windows 用語との間で意味が異なることがあります。これはやむなしとしても、.NET Framework 用語と Windows 用語との間でさえ統一されているとはいえない状況です。アプリケーション開発者にとって唯一の手がかりである SDK の解説も例外ではありません。Microsoft にはドキュメントの内容をチェックする体制がないのでしょうか。ともあれ、このページでは並列コンピューティングに関係する用語をあらためて整理してみることにしました。
なお、ここでは用語全般の説明が目的ではありません。あくまで並列コンピューティングにかかわる用語としての解説であることを強調しておきたいと思います。また、私自身の想像も含まれることをお断りしておきます。
オペレーティングシステムとしての Windows は、実行中のアプリケーション(EXE ファイルおよびそれに関連する DLL ファイルを含む)をプロセス "process" と呼ぶ管理単位を使って制御します。一つのプロセスから別のプロセスに直接アクセスすることはできません。
プロセス間の通信はディスク上のファイルやクリップボード経由で可能ですが、例外的にはメモリマップトファイル(.NET Framework 4.0 に追加された MemoryMappedFile クラスを参照)を使う手もあります。
プロセスは一つあるいは複数のスレッド "thread" 上で実行されます。つまり、スレッドとはオペレーティングシステムがプロセッサの処理時間を割り当てるときの基本単位です。
マルチスレッドについて少し触れておきます。プロセッサが 1 つしかないパソコンの場合、マルチスレッドにしたからといって速くなるわけではありません。1 秒かかる処理が 2 つあって、マルチスレッドで動作させたとしても 2 つの処理が終わるまでに 2 秒かかります。したがって、マルチスレッドは CPU 占有率の高い処理と低い処理とを組み合わせるときにもっとも大きなメリットを得ることができます。たとえば、WEB からファイルサイズの大きなファイルをダウンロードする処理と UI 操作との組み合わせです。UI 操作は人間がキーボードやマウスを使って実行しますから、ほとんどの時間がアイドリング状態になり、ファイルのダウンロードのほうにより多くの処理時間を割り当てることができます。つまり、UI 操作をしているうちにファイルのダウンロードが終了するので、2 つの処理が同時進行したような感覚を与える効果があります。
Windows の場合、マルチスレッドの各スレッドに対する処理時間は 20 ミリ秒ごとに割り当てます。
ところで、.NET Framework 4.0 にタスクという概念が導入されました。これは並列コンピューティングを実現するときに、スレッドを直接操作していてはアプリケーション開発者への負担が多くなりすぎますので、操作を抽象化する目的で導入されたものと理解しています。続きは、タスクの項をご覧ください。
【追 記】
タスクという概念が追加されるまで、マルチスレッドはスレッドを直接操作しなければなりませんでした。しかし、タスクの導入とともにスレッドの意味が変わったように思います。つまり、スレッドはコードを実行するときの管理単位であると同時に実行単位でもあったわけですが、タスクという概念が導入されたことで、管理単位としての役割をタスクにゆずったと考えます。したがって、タスクを実行するときにそのためのスレッドが形成されるということになります。
ある WEB サイトを見ると、スレッドの中にタスクが存在するかのような解説がありましたが、正確な表現ではないかもしれません。タスクを実行するときにスレッドが生成されますが、私はタスクの要求によってタスクスケジューラがスレッドを生成すると理解しています。したがって、タスクがスレッドに属するということはないと考えます。
スレッドセーフ型 "thread-safe type" とは、マスチスレッドで動作しているときに同期するための仕組みを用意しなくても複数のスレッドから同時にアクセス可能な型です。つまり、同時に一つのスレッドからのみのアクセスを可能にします。.NET Framework 4.0 には System.Collections.Concurrent 名前空間内にスレッドセーフなコレクションクラスが追加されました。
.NET Framework にはスレッドプール "thread pool" という概念があります。"pool" は「泳ぐプール」という意味のほかに、「共同管理」という意味もあります。したがって、スレッドプールとは「スレッドの共同管理」でいいのかもしれません。
さて、.NET Framework 4.0 では並列処理を可能にするため、ThreadPool クラスが大幅に変更されました。「されました」と断言しましたが、その内容はよく分かっていません。内部的な問題は詮索してもキリがありませんので、これぐらいで勘弁してください。
本ページは並列処理がテーマですからそれを中心にして説明しますが、私の想像を交えての説明ですので、正しくないかもしれません。.NET Framework SDK に解説がないことがその原因ですので、ご容赦を!
.NET Framework ではプロセスに対して一つの ThreadPool オブジェクトを作成し、一つのスレッドを与えます。スレッドは一つの SynchronizationContext オブジェクト(以後、簡単のため、同期コンテキストと称す)を作成し、保持します。これはデフォルトの同期コンテキストとなります。コンソールアプリケーションや Windows サービスアプリケーションの場合は ThreadPool スレッドだけを持ちますが、Windows フォームアプリケーションおよび WPF アプリケーションは ThreadPool スレッドと UI スレッドとの 2 つのスレッドが与えられ、UI スレッドも一つの同期コンテキストを持ちます
UI スレッド用同期コンテキストは、最初にコントロールを作成したときに構築されます。フォームを持ついわゆる UI アプリケーションの場合は、Form オブジェクト(WPF アプリケーションの場合は Window オブジェクト)を作成したときです。
ThreadPool スレッド用および UI スレッド用に同期コンテキストが自動的に作られると説明しましたが、TaskScheduler クラスのインスタンスも作成されるものと想像します。というのは、TaskScheduler クラスには FromCurrentSynchronizationContext メソッドがあって、カレントの同期コンテキストからデフォルトのタスクスケジューラを取得することができます。また、ズバリ、Default プロパティがあって、ThreadPool スレッドに与えられたと思われるデフォルトのタスクスケジューラを取得することができます。つまり、スレッドと同様に、ThreadPool スレッドと UI スレッドの両方にタスクスケジューラが作成されるようです。
先に少し触れましたが、タスクスケジューラがタスクの実行を制御していると考えます。
.NET Framework 4.0 の SDK の中に「マネージスレッド処理」-「フォアグラウンドスレッドとバックグラウンドスレッド」という項目はありますが、フォアグラウンドスレッドとは何か、バックグラウンドスレッドとは何かについての説明はぼんやりしています。両者の違いを明らかにしておきましょう。
一言で言えば、プロセス終了時の動作の違いです。フォアグラウンドスレッドの場合、スレッドの処理が実行中の間はプロセスを終了しようとしてもスレッドにブロックされてしまって、プロセスを終了することはできません(ただし、フォームは消去されるので、一見するとアプリケーションが終了したように見える)。一方、バックグラウンドスレッドの場合は、スレッドの終了を待たずにプロセスは終了し、プロセスはすべてのバックグラウンドスレッドの Abort メソッドを強制的に呼び出します。
ちなみに、Thread クラスの IsBackground プロパティは false です。つまり、Thread クラスのインスタンスはデフォルトではフォアグラウンドスレッドになります。
ワーカースレッド "worker thread" のワーカーとは誰なのか気になってしかたがない。WEB サイトをうろついてみましたが、解決しませんでした。アプリケーション(正確にはプロセスですが)には必ず一つのスレッドが与えられます、これをメインスレッドとかプライマリスレッドと呼びます。これとは別に補助的に作成するスレッドをワーカースレッドと呼ぶようです。ということは、ワーカーはアプリケーション開発者ということでいいのでしょうか。
.NET Framework ではオペレーティングシステムプロセスをアプリケーションドメイン "application domain" と呼ぶマネージサブプロセスに分割します。アプリケーションドメインはこのページの趣旨とは異なりますので、これ以上の説明は省略します。
タスク "task" の英語としての意味は、「一定の期間内で義務としてやり遂げなければならない任務」です。"Task Force" という言葉でおなじみですね。
Task クラスが.NET Framework 4.0 に追加されましたが、.NET Framework SDK の中に「タスクは、特定のジョブを実行する作業の単位です。」とあります。「ジョブ」とは何かについての説明がないので、依然として分かりにくいのですが、この文章におけるジョブは「処理」と読み替えてもいいと思います。つまり、.NET Framework の並列コンピュータ用語としてのタスクは「非同期的に実行する作業単位」でいいと思います。ただし、決してスレッドと同じではありません。「スレッド」の項で触れましたが、並列コンピューティングを実現するためにスレッドを直接操作することなく、特定のスレッドの終了を待機したり、複数のすべてのスレッドの実行が終了するまで待機したり、あるいはスレッドの実行をキャンセルしたりといろいろな操作を抽象化する目的で導入された概念だと思います。
.NET Framework 4.0 のタスクの裏側では、ThreadPool が動作していることは確実です。.NET Framework 4.0 の並列処理では ThreadPool を直接操作するのではなく、タスクを使いなさいということです。.NET Framework 4.0 とワザワザ .NET Framework のバージョン番号を付記したのはこれまでの .NET Framework のタスクとは内容がまったく異なることを再度強調しておきます。
話は変わるようですが、タスクは子タスクを持つことができます。つまり、入れ子にすることが可能です。
.NET Framework 4.0 に TaskScheduler クラスが追加されました。タスクをスケジューリングするクラスという理解でいいと思います。スケジューリングとは、タスクを実行する順番を管理することです。
TaskScheduler クラスは、内部的には ThreadPool クラスを使っているようです。プロセッサのコアの数だけのワーカースレッド "worker thread"(実際に動作可能なスレッド)が存在可能ですが、それを超えるタスクが存在する場合、タスクスケジューラはタスクにワーカースレッドを割り当てるまでタスクの実行の順番を制御(後述するキューに登録すること)したり、待機したりします。
継続タスク "continuation task" とは、ほかのタスクが終了したときに自動的にスタートするタスクです。まず、以下のコードを見比べてください。
private void buttonl_Click(object sender, RoutedEventArgs e) { Debug.WriteLine(String.Format("current thread: {0}", Thread.CurrentThread.ManagedThreadId)); var task = Task<int>.Factory.StartNew(() => { Debug.WriteLine(String.Format("task thread: {0}", Thread.CurrentThread.ManagedThreadId)); Thread.Sleep(1000); return 2; }); Debug.WriteLine(String.Format("Result: {0} {1}", task.Result, Thread.CurrentThread.ManagedThreadId)); }
実行結果:
current thread: 9 task thread: 10 Result: 2 9
この例では、task.Result を待つためにメインスレッドもここで止まってしまいます。したがって、並列処理になりません。そこで、継続タスクを使います。
private void button2_Click(object sender, RoutedEventArgs e) { Debug.WriteLine(String.Format("current thread: {0}", Thread.CurrentThread.ManagedThreadId)); Task<int>.Factory.StartNew(() => { Debug.WriteLine(String.Format("task thread: {0}", Thread.CurrentThread.ManagedThreadId)); Thread.Sleep(1000); return 2; }) .ContinueWith(task => { Debug.WriteLine(String.Format("Result: {0} {1}", task.Result, Thread.CurrentThread.ManagedThreadId)); }); }
実行結果:
current thread: 9 task thread: 11 Result: 2 10
継続タスクを使うと、メインスレッドとは別のスレッドで task.Result を待機することになります。
先行タスク "antecedent task" とは、継続タスクの前に実行したタスクです。
ブロック "block" とは、ある条件を満足するまで実行を停止することです。よい意味の場合と悪い意味の場合とがあります。つまり、よい場合とは意図しない実行を抑制する場合で、悪い場合とは停止したくないのに実行が止まってしまうことです。にっちもさっちも行かなくなる状態をデッドロック "dead lock" と呼びます。
パーティション "partition" とは、大規模なデータを複数に分割して処理するときの一つのデータの塊をさします。
タスクスケジューラの項で、TaskScheduler クラスは内部的には ThreadPool クラスを使っていると説明しました。ThreadPool クラスは .NET Framework 4.0 で新規開店になったようで、旧バージョンのそれとはまったく異なる思想のもとに構築されました。
つまり、旧バージョンのそれは実行すべき項目(新しい概念としてのタスクですね)を一つのキューで管理していました。これをグローバルキューと呼ぶことになるのですが、このキューには一度に一つのスレッドからしかアクセスすることができません。これではプロセッサが複数のコア(2011 年の時点では 2 つか 4 つですが、将来はもっと増えるでしょう)を持っているとしてもスレッドを同期させるためのコスト(犠牲という意味です)が高くなります。ここでローカルキューに分散するという発想が生まれました。
ローカルキューはスレッドプールのそれぞれのワーカースレッドに割り当てられます。グローバルキューはこれとは別に存在します。ローカルキューに処理を分散させることで、一つ一つは小さいが数百万のタスクがあってもスケジューリングが可能になります。
新しい言葉が出てきました。ワークスティーリング "Work Stealing" です。"stealing" は「盗む」です。ローカルキューの一つに実行項目があって、そのほかのローカルキューとグローバルキューに実行項目がない場合、実行項目を持つローカルキューから残っている項目を取ってきて実行することです。
フェーズ "phase" もいろいろな場面で使う言葉なので、何をさすのかをすぐに理解することは難しいと思います(アメリカ人ならすぐに分かるのかもしれませんが)。.NET Framework の並列コンピューティング用語としては、Barrier クラスに関連する場面でよく出てきますが、その意味の説明はありません。しかし、タスクの中の個々の局面ぐらいでいいと思います。具体的に文章で説明するのは難しいので、Barrier クラスのページをご覧ください。
.NET Framework 4.0 には SpinLock 構造体と SpinWait 構造体とが追加されました。これらの名前の中にある「スピン」とは何かですが、スピン "spin" は「クルクル回転する」でいいでしょう。話が変わるようですが、伝統的な Windows プログラミングでは Windows メッセージを受け取る無限ループを形成します。スピンとはこのようなものと考えていいと思います。つまり、何らかの処理(タスク)が終了したかどうかなどをチェックするための仕組みです。したがって、一つ一つの処理(タスク)の処理時間がきわめて短い場合に利用します。
プリエンプティブ "preemptive" の英語としての意味は、「先買権のある」で、ほかに優先して買い取る権利です。通常はプリエンプティブマルチスレッドというような使い方をします。「スレッド」の項で、「プロセッサの処理時間を割り当てる」と説明しましたが、オペレーティングシステムがこれを強制的に実行することです。したがって、スレッドの都合やスレッドが動作していない場合でもスレッドの切り替えが強制されます。
一方、強調的マルチスレッドというものもあります。これはスレッドのほうで自主的に処理をオペレーティングシステムに戻してもらうものです。Windows におけるマルチスレッドは通常プリエンプティブです。
ThreadPool クラスは元からありましたが、.NET Framework 4.0 では内部的な処理が大幅に変わったようです。「タスク」の項で説明したとおり、タスクはスレッドプールを使っているようなので、そのための改良と思われます。
ともあれ、スレッドプールとはマネージスレッドのコレクションで、スレッドの作成あるいは破棄のためのオーバーヘッド(スレッドを構築・維持するための処理)をできるだけ小さくするための仕組みを持ちます。タスクはスレッドプール内のスレッド上で動作します。
.NET Framework 4.0 に追加された Barrier クラスは同期オブジェクトの一種で、一つのフェーズの終了を意味します。バリア "barrier" は、「障壁」のほかに「駅の改札口」という意味もあります。つまり、登録した複数のすべてのスレッドが所定のポイント(これをバリアと呼ぶ)に到達するまでそれ以上の実行を抑制する仕組みです。 フェーズの終了後、つまり、バリアに達したあとに実行すべきことをフェーズ後の処理 "post-phase action" と呼びます。 フェーズについては「フェーズ」の項を見てください。
パイプライン "pipeline" の英語としての意味は、「石油のパイプライン」、「流通ルート」のほかに「情報ルート」もあります。要するに、一方から入って他方から出てくるものをさすようです。日本風に言えば、タバコをすう「キセル」のほうがピンとくるかもしれません。つまり、タバコの葉を燃やして煙を出し、吸い口のほうから出てくる状態をイメージしてください。
並列コンピューティング用語としての意味は以下のとおりです。
A series of producer/consumers, where each one consumes the output produced by its predecessor.
直訳すると、「一連の生産者と消費者との組み合わせで、直前の生産者あるいは消費者によって生成された成果物をそれぞれが消費する」ぐらいでしょうか。生産者はデータを作成する人、消費者はデータを利用する人でいいと思います。消費者は生産者が作成したデータを利用し、新しいデータを作成します。したがって、消費者は生産者でもあるわけです。
さて、.NET Framework 4.0 に IProducerConsumerCollection<T> インターフェースが追加されました。このインターフェースの実装クラスは System.Collections.Concurrent.BlockingCollection<T> クラスです。ほかには ConcurrentQueue<T> クラスなどがあります。これらのクラスの詳細は NETClass を見てください。いずれにしろ、パイプラインはプログラミング手法の名前にすぎません。PipeLine クラスのようなものはありません。
わたしは JAVA を知らないので、確信はありませんが、プロデューサー/コンシューマー パターン "Producer/Consumer Pattern" は JAVA に起源があるのではないかと思います。そうだとすると、それをベースにして説明するほかありません。
プロデューサー "producer" は生産者、コンシューマー "Consumer" は消費者です。要するに、データを作成する人とそれを使う人でいいと思います。消費者は受け取ったデータを加工する生産者にもなりえます。しかも、データを受け取ったあと、データを削除するので、消費者と呼ぶ由来になっているようです。こういう関係をプロデューサー/コンシューマー パターンと呼び出す。
プロデューサー/コンシューマー パターンを実現する .NET Frameworkの機能は、BlockingCollection クラスです。
−以上−