Visual Basic .NET でのマルチスレッド プログラミング

Robert Burns
Visual Studio Team
Microsoft Corporation

February 2002
日本語版最終更新日 2003 年 5 月 6 日

要約 : .NET Framework では、マルチスレッド アプリケーションを簡単に作成するための新しいクラスが用意されています。この文書では、Microsoft® Visual Basic® .NET でマルチスレッド プログラミング テクニックを使用して、さらに効率的で高速なアプリケーションを作成する方法を解説します。

目次

はじめに
マルチスレッド処理の利点
新規スレッドの作成
スレッドの同期
スレッド タイマ
タスクのキャンセル
まとめ

はじめに

従来、Visual Basic 開発者は、タスクを順次実行する同期アプリケーションを作成してきました。マルチスレッド アプリケーションは複数のタスクをほとんど同時に実行できるため効率的ですが、以前のバージョンの Visual Basic で作成するのは困難でした。

マルチスレッド プログラムは、オペレーティング システムが持つマルチタスキングと呼ばれる機能により、可能となっています。マルチタスキングは、複数アプリケーションの同時実行機能をシミュレートするものです。ほとんどのパーソナル コンピュータに搭載されているプロセッサは 1 つですが、スレッドと呼ばれる複数の実行可能コードにプロセッサ時間を割り振ることにより、最近のオペレーティング システムはマルチタスキングを実現しています。1 つのスレッドが 1 つのアプリケーション全体を意味することもありますが、通常は個別に実行可能なアプリケーションの一部分です。オペレーティング システムでは、スレッドの優先順位やスレッドが最後に実行されてからの経過時間などに基づいて、各スレッドに処理時間が配分されます。ファイルの入出力など、一定の時間に集中するタスクを実行する場合、複数のスレッドによってパフォーマンスを大幅に向上することができます。

ただし、注意すべき点が 1 つあります。複数のスレッドによってパフォーマンスを向上することができますが、スレッドを作成するために必要なメモリや、実行するためのプロセッサ時間など、各スレッドにはコストもかかります。スレッドを多く作りすぎると、実際にはアプリケーションのパフォーマンスが低下する場合もあります。マルチスレッド アプリケーションをデザインする場合、スレッドを追加する利点とコストを比較検討する必要があります。

マルチタスキングは以前から、オペレーティング システムの一部でした。しかし最近まで、Visual Basic プログラマがマルチスレッド タスクを実行するには、公開されていない機能を使用するか、オペレーティング システムの非同期パーツまたは COM コンポーネントを使用して間接的に行うしかありませんでした。.NET Framework ではマルチスレッド アプリケーション開発のために、System.Threading 名前空間で包括的なサポートを提供しています。

この文書では、マルチスレッドのいくつかの利点と、Visual Basic .NET を使用してマルチスレッド アプリケーションを開発する方法について解説します。Visual Basic .NET と .NET Framework でマルチスレッド アプリケーションを開発することは容易ですが、この文書は中級から上級開発者、および以前のバージョンの Visual Basic から Visual Basic .NET に移行中の開発者を対象としています。Visual Basic .NET について初心者の方は、まず 「Visual Basic 言語のツアー」 のトピックに目を通してください。

マルチスレッド プログラミングの包括的な解説は、この文書の目的ではありませんが、巻末で紹介する参照先でさらに情報を得ることができます。

マルチスレッド処理の利点

同期アプリケーションの方が開発するのは簡単ですが、前のタスクが完了しないと次のタスクを開始できないため、効率性の面でマルチスレッド アプリケーションより劣ります。同期タスクに予想以上の時間がかかると、アプリケーションが動作していないように見えることもあります。マルチスレッド処理では、複数の処理を同時に実行できます。たとえばワード プロセッサでは、ユーザーがドキュメントを作成している間でも、スペル チェックを別のタスクとして実行できます。マルチスレッド アプリケーションでは、プログラムを独立したタスクに分割するため、パフォーマンスを以下のように大幅に向上できます。

  • 別のタスクを実行中でもユーザー インターフェイスをアクティブしておけるため、プログラムの応答が速くなります。
  • 現在ビジーではないタスクのプロセッサ時間を、他のタスクに譲ることができます。
  • 多大な処理時間を要するタスクの場合は、定期的にそのプロセッサ時間を他のタスクに譲ることができます。
  • タスクをいつでも停止できます。
  • タスクの優先度を個別に調整して、パフォーマンスを最適化できます。

マルチスレッド アプリケーション作成の是非を判断する要因にはいくつかあります。マルチスレッドは、以下のような場合に最適です。

  • 時間を消費するタスクやプロセッサに負荷のかかるタスクによって、ユーザー インターフェイスがブロックされる場合
  • リモート ファイルやインターネット接続など、外部リソースを個別のタスクで待機する必要がある場合

たとえば、インターネット "ロボット" について考えてみます。これは、Web ページのリンクをたどり、特定の条件を満たすファイルをダウンロードするアプリケーションです。このようなアプリケーションでは、ファイルを 1 つずつ同期的にダウンロードすることもできれば、マルチスレッドを使用して複数のファイルを同時にダウンロードすることもできます。このような用途では、マルチスレッド アプローチの方が、同期的アプローチよりもはるかに効率的です。それは、リモート Web サーバーからの遅い応答を受信しているスレッドがいくつかあっても、複数のファイルをダウンロードできるためです。

新規スレッドの作成

最もシンプルなスレッド作成の方法は、スレッド クラスの新規インスタンスを作成し、AddressOf ステートメントを使用して、実行するプロシージャのデリゲートを渡すことです。たとえば以下のコードでは、SomeTask という名前のサブ プロシージャを個別のスレッドとして実行します。

Dim Thread1 As New System.Threading.Thread(AddressOf SomeTask)
Thread1.Start
' ここに置かれるコードは直ちに実行されます。

スレッドの作成と開始はこれだけで済みます。スレッドの Start メソッドの呼び出しに続くコードはすべて、その前のスレッド完了を待たずにすぐ実行されます。

以下の表は、個別スレッドを制御するために使用できるメソッドをいくつか示しています。

メソッド 動作
Start スレッドの実行開始をトリガします。
Sleep 指定した時間、スレッドを停止します。
Suspend セーフ ポイントに達するとスレッドを停止します。
Abort セーフ ポイントに達するとスレッドを終了します。
Resume 停止したスレッドを再開します。
Join 現在のスレッドを、他のスレッドが完了するまで待機させます。タイムアウト値を使用した場合、割り当てられた時間内にスレッドが完了すると、このメソッドは値 True を返します。

これらのメソッドの意味は明らかですが、セーフ ポイントの概念は目新しいかもしれません。セーフ ポイントとは、Common Language Runtime がコード内で "ガベージ コレクション" を安全に自動実行できる場所のことです。ガベージ コレクションとは、使用していない変数を解放してメモリを増加させるプロセスです。スレッドで Abort メソッドまたは Suspend メソッドを呼び出すと、Common Language Runtime がコードを分析し、スレッドの実行を停止する適切な位置を決定します。

スレッドには以下の表に示すように、有用なプロパティもいくつか含まれています。

プロパティ名
IsAlive スレッドがアクティブな場合、値が True になります。
IsBackground スレッドがバックグラウンド スレッドである場合は、ブール値が取得されます。また、スレッドをバック グラウンド スレッドにする場合、ブール値を設定します。バックグラウンド スレッドはフォアグラウンド スレッドと同様のものですが、バックグラウンド スレッドが実行されていても、プロセスを終了できます。プロセス内のフォアグラウンド スレッドがすべて終了すると、Common Language Runtime は実行中のバックグラウンド スレッドに対して Abort メソッドを呼び出し、プロセスを終了します。
Name スレッドの名前を取得または設定します。デバッグ中に個別のスレッドを見つけるため、最もよく使用するプロパティです。
Priority スレッド スケジュールの優先順位を設定するため、オペレーティング システムによって使用される値を取得または設定します。
ApartmentState 特定のスレッドで使用するスレッド モデルを取得または設定します。スレッド モデルは、スレッドがアンマネージ コードを呼び出す場合に重要となります。
ThreadState スレッドの状態を表す値が含まれます。

スレッド プロパティとメソッドは、スレッドの作成および管理に便利です。この文書の「スレッドの同期」セクションでは、プロパティとメソッドを使用してスレッドを制御、構成する方法を説明します。

スレッド引数と戻り値

前述の例にあるメソッド呼び出しには、パラメータや戻り値を含めることはできません。この制限は、前述の方法によるスレッド作成および実行の主な欠点の 1 つです。ただし、個別のスレッドで実行するプロシージャとの間で引数を受け渡すことはできます。そのためには、スレッドをクラスまたは構造体にラップします。

Class TasksClass
   Friend StrArg As String
   Friend RetVal As Boolean
   Sub SomeTask()
      ' StrArg フィールドを引数として使用します。
      MsgBox("StrArg には文字列" & StrArg & "が含まれます。")
      RetVal = True ' 戻り引数の戻り値を設定します。
   End Sub
End Class
' クラスを使用するには、パラメータを格納するフィールドまたはプロパティを
' 設定し、必要に応じて非同期的にメソッドを呼び出します。
Sub DoWork()
   Dim Tasks As New TasksClass()
   Dim Thread1 As New System.Threading.Thread( _
       AddressOf Tasks.SomeTask)
   Tasks.StrArg = "Some Arg" ' 引数として使用するフィールドを設定します。
   Thread1.Start() ' 新しいスレッドを開始します。
   Thread1.Join() ' スレッド 1 の終了を待ちます。
   ' 戻り値を表示します。
   MsgBox("スレッド 1 が値" & Tasks.RetVal "を返しました。")
End Sub

スレッドの優先順位やスレッド モデルを細かく制御するアプリケーションでは、手動によるスレッドの作成と管理が最適です。ただこの方法では、多くのスレッドを管理するのは困難な場合があります。多くのスレッドが必要な場合は、複雑さを減らすためにスレッド プーリングの使用を考慮してください。

スレッド プーリング

スレッド プーリングはマルチスレッドの一形態で、タスクをキューに入れておき、スレッドが作成されると、キューにあるタスクを自動的に開始する手法です。スレッド プーリングでは、実行するプロシージャのデリゲートを使用して、Threadpool.QueueUserWorkItem メソッドを呼び出します。これによって、Visual Basic .NET でスレッドが作成され、プロシージャが実行されます。以下の例は、スレッド プーリングを使用して複数のタスクを開始する方法を示しています。

Sub DoWork()
   Dim TPool As System.Threading.ThreadPool
   ' タスクをキューに入れます。
   TPool.QueueUserWorkItem(New System.Threading.WaitCallback _
                          (AddressOf SomeLongTask))
   ' 別のタスクをキューに入れます。
   TPool.QueueUserWorkItem(New System.Threading.WaitCallback _
                          (AddressOf AnotherLongTask))
End Sub

各スレッドのプロパティを個別に設定せずに多くの個別タスクを開始する場合は、スレッド プーリングが便利です。各スレッドは、既定のスタック サイズと優先度で開始します。既定では、システム プロセッサごとに最大 25 のスレッドをスレッド プールで実行できます。この制限を超えるスレッドをキューに入れることもできますが、他のスレッドが完了するまで開始されません。

スレッド プーリングの利点として、状態オブジェクトの引数をタスク プロシージャに渡せるということがあります。呼び出しているプロシージャに複数の引数が必要な場合、クラスのインスタンスまたは構造体を Object データ型にキャストできます。

パラメータと戻り値

スレッド プール スレッドからの戻り値を扱うには、少し注意が必要です。スレッド プールのキューに追加できるプロシージャは Sub プロシージャのみのため、関数呼び出しから値を返すという標準の方法は実行できません。パラメータの設定と戻り値の取得を実現するには、「スレッド引数と戻り値」で説明したように、パラメータ、戻り値、およびメソッドをラッパ クラスにラップする方法があります。簡単な方法としては、QueueUserWorkItem メソッドの ByVal 状態オブジェクト変数 (オプション) を使用することができます。この変数を使用してクラスのインスタンスへの参照を渡すと、インスタンスのメンバはスレッド プールのスレッドによって修正され、戻り値として使用されます。値で渡された変数で参照されるオブジェクトを修正できるという点が、最初は理解しにくいかもしれません。これが可能なのは、値によって渡されるのがオブジェクト参照のみであるためです。オブジェクト参照によって参照されるオブジェクトのメンバに変更を加えると、その変更は実際のクラス インスタンスにも反映されます。

状態オブジェクト内の値を返すために、構造体を使用することはできません。構造体は値の種類であるため、非同期プロセスによって加えられた変更は、元の構造体のメンバに反映されません。戻り値が不要な場合は、構造体を使用してパラメータを設定できます。

Friend Class StateObj
   Friend StrArg As String
   Friend IntArg As Integer
   Friend RetVal As String
End Class

Sub ThreadPoolTest()
   Dim TPool As System.Threading.ThreadPool
   Dim StObj1 As New StateObj()
   Dim StObj2 As New StateObj()
   ' 状態オブジェクト内でパラメータのように機能するフィールドをいくつか設定します。
   StObj1.IntArg = 10
   StObj1.StrArg = "文字列"
   StObj2.IntArg = 100
   StObj2.StrArg = "別の文字列"
   ' タスクをキューに追加します。
   TPool.QueueUserWorkItem(New System.Threading.WaitCallback _
                          (AddressOf SomeOtherTask), StObj1)
   ' 別のタスクをキューに入れます。
   TPool.QueueUserWorkItem(New System.Threading.WaitCallback _
                          (AddressOf AnotherTask), StObj2)
End Sub

Sub SomeOtherTask(ByVal StateObj As Object)
   ' 状態オブジェクト フィールドを引数として使用します。
   Dim StObj As StateObj
   StObj = CType(StateObj, StateObj)   ' 正しいタイプにキャストします。
   MsgBox("StrArg には文字列" & StObj.StrArg & "が含まれます。")
   MsgBox("IntArg には数値" & CStr(StObj.IntArg) & "が含まれます。")
   ' フィールドを戻り値として使用します。
   StObj.RetVal = "SomeOtherTask からの戻り値"
End Sub

Sub AnotherTask(ByVal StateObj As Object)
   ' 状態オブジェクト フィールドを引数として使用します。
   ' 状態オブジェクトがオブジェクトとして渡されます。
   ' これをその特定の型にキャストすると、容易に使用できるようになります。
   Dim StObj As StateObj
   StObj = CType(StateObj, StateObj)
   MsgBox("StrArg には文字列" & StObj.StrArg &"が含まれます。")
   MsgBox("IntArg には数値" & CStr(StObj.IntArg) &"が含まれます。")
   ' フィールドを戻り値として使用します。
   StObj.RetVal = "AnotherTask からの戻り値"
End Sub

Common Language Runtime によって、キューで待機しているスレッド プール タスクに対するスレッドが自動的に作成され、タスクが完了するとそれらのリソースは解放されます。タスクをいったんキューに入れると、簡単にキャンセルする方法はありません。ThreadPool スレッドは常に、マルチスレッド アパートメント (MTA) スレッド モデルを使用して実行されます。単一スレッド アパートメント モデル (STA) を使用するスレッドの場合は、手動でスレッドを作成してください。

スレッドの同期

同期により、マルチスレッド プログラミングの非構造性と同期処理の構造性を適切に組み合わせることができます。

同期テクニックを使用すると、以下のことが可能になります。

  • タスクを特定の順序で実行する必要がある場合、コードを実行する順序を明示的に指定できる。
  • 2 つのスレッドが同じリソースを同時に共有する場合の問題発生を防止できる。

たとえば同期化を使用すると、別のスレッドを実行しているデータ取得プロシージャが完了するまで、表示プロシージャを待機させることができます。

同期の方法にはポーリングと、同期オブジェクトを使用する方法の 2 種類があります。ポーリングでは、ループ内部からの非同期呼び出しの状態が繰り返しチェックされます。ポーリングでは、さまざまなスレッドのプロパティのステータス チェックにリソースを消費するため、スレッド管理の効率は低くなります。

たとえば、IsAlive プロパティを使用してスレッドが終了したか確認できます。ただし、終了していないスレッドでも実際に実行中とは限らないため、このプロパティの使用には注意が必要です。スレッドの ThreadState プロパティを使用すると、スレッド ステータスの詳細情報を得ることができます。スレッドは同時に複数のステータスを持つ場合もあるため、ThreadState の値は、System.Threading.Threadstate 列挙の値の組み合わせになる場合もあります。このため、ポーリングの場合は関連スレッドのステータスを入念に確認する必要があります。たとえば、スレッドのステータスが "実行中" ではない場合、そのスレッドは既に終了している可能性があります。しかし、停止中あるいはスリープ中の可能性もあります。

スレッドを実行する順序の制御と引き替えに、ポーリングではマルチスレッドの利点がいくらか損なわれることになります。Join メソッドを使用してスレッドを管理するアプローチの方が効率的と言えます。Join を使用すると、スレッドが終了するか、タイムアウトを指定した場合はタイムアウトになるまで、呼び出し側プロシージャは待機状態となります。Join という名前は、新規スレッドの作成が実行パス内の合流点であるという考え方に基づいています。Join を使用すると、分離した実行パスを再び 1 つのスレッドに統合することができます。

図 1. スレッド

1 つ明らかなことは、Join が同期呼び出し、つまりブロック呼び出しであるという点です。Join または待ちハンドルの Wait メソッドを呼び出すと、呼び出し側プロシージャは停止し、スレッド終了の通知を待ちます。

Sub JoinThreads()
   Dim Thread1 As New System.Threading.Thread(AddressOf SomeTask)
   Thread1.Start()
   Thread1.Join()      ' スレッドの終了を待ちます。
   MsgBox("スレッドが終了しました。")
End Sub

スレッドを制御するこれらの簡便法は、少ないスレッドを管理する場合には便利ですが、大きなプロジェクトでは使用が困難になります。次のセクションでは、スレッドを同期する上級テクニックについて解説します。

同期の上級テクニック

マルチスレッド アプリケーションでは、待ちハンドルおよびモニタ オブジェクトを使用して複数のスレッドを同期させるケースが多くあります。以下の表は、スレッドの同期で使用できる .NET Framework クラスをいくつか示しています。

クラス 目的
AutoResetEvent イベントが発生したことを、待機している 1 つ以上のスレッドに通知する待ちハンドルです。待機中のスレッドが解放されると、AutoResetEvent は自動的にステータスを通知済みに変更します。
Interlocked 複数のスレッドによって共有される変数のアトミック処理を可能にします。
ManualResetEvent イベントが発生したことを、待機している 1 つ以上のスレッドに通知する待ちハンドルです。手動でリセットしたイベントのステータスは、Reset メソッドによって未通知に設定されるまで、通知済みのままです。同様に、未通知のステータスは Set メソッドによって通知済みに設定されるまでそのままです。待機中のスレッド、あるいは待ち関数の 1 つを呼び出すことによって指定イベントの待機処理を開始する予定のスレッドはいくつでも、オブジェクトのステータスが通知済みの間に解放できます。
Monitor オブジェクトへのアクセスを同期化するメカニズムを提供します。Visual Basic .NET アプリケーションでは、SyncLock を呼び出してモニタ オブジェクトを使用します。
Mutex プロセス間の同期に使用できる待ちハンドルです。
ReaderWriterLock 単独ライターと複数リーダーのセマンティクスを設定するロックを定義します。
Timer 指定した間隔でタスクを実行するメカニズムを提供します。
WaitHandle 共有リソースへの排他的アクセスを待機する、オペレーティング システム固有のオブジェクトをカプセル化します。

待ちハンドル

待ちハンドルは、あるスレッドのステータスを別のスレッドに通知するオブジェクトです。スレッドは待ちハンドルを使用して、リソースへの排他的アクセスが必要なことを他のスレッドに通知できます。通知を受け取ったスレッドは、待ちハンドルが使用されなくなるまで、そのリソースの使用を控える必要があります。待ちハンドルには、通知済みと未通知の 2 つのステータスがあります。どのスレッドにも所有されていない待ちハンドルのステータスは通知済みです。スレッドに所有されている待ちハンドルのステータスは未通知です。

WaitOne、WaitAny、WaitAll などの待ちメソッドを呼び出すことにより、スレッドは待ちハンドルの所有権を要求します。個別スレッドの Join メソッドと同じく、待ちメソッドはブロック呼び出しです。

  • 他のスレッドが待ちハンドルを所有していなければ、すぐに True が返されて待ちハンドルのステータスが未通知に変わり、その待ちハンドルを所有するスレッドが引き続き実行されます。
  • スレッドが待ちハンドルの待ちメソッドを 1 つ呼び出したとき、その待ちハンドルが別のスレッドに所有されていた場合、呼び出し側スレッドは他のスレッドが待ちハンドルを解放するまで一定時間 (タイムアウトが指定されている場合)、またはいつまでも (タイムアウトが指定されていない場合) 待ちます。タイムアウトが指定されていて、待ちハンドルがタイムアウトの指定時間内に解放された場合は、True が返されます。待ちハンドルが解放されない場合は False が返され、呼び出し側スレッドは実行を継続します。

待ちハンドルを所有するスレッドは、終了時あるいは待ちハンドルを必要としなくなった時点で Set メソッドを呼び出します。他のスレッドから、待ちハンドルのステータスを未通知にリセットできます。そのためには、Reset メソッドを呼び出すか、WaitOne、WaitAll、WaitAny のいずれかを呼び出してスレッドが正常に Set を呼び出すのを待ちます。AutoResetEvent ハンドルは、単独で待機中のスレッドが解放されると、システムによって自動的に未通知にリセットされます。待機中のスレッドがない場合は、イベント オブジェクトのステータスは通知済みのままです。

メソッド 目的
WaitOne 待ちハンドルを引数として受け入れ、Set を呼び出したスレッドから現在の待ちハンドルに通知があるまで、呼び出し側スレッドを待たせます。
WaitAny 待ちハンドルの配列を引数として受け入れ、指定した待ちハンドルのいずれかに Set 呼び出しによる通知があるまで、呼び出し側スレッドを待たせます。
WaitAll 待ちハンドルの配列を引数として受け入れ、指定した待ちハンドルのすべてに Set 呼び出しによる通知があるまで、呼び出し側スレッドを待たせます。
Set 指定した待ちハンドルのステータスを通知済みに設定し、待機中のスレッドを再開させます。
Reset 指定したイベントのステータスを未通知に設定します。

Visual Basic .NET で一般的に使用される待ちハンドルには、Mutex オブジェクト、ManualResetEvent、AutoResetEvent の 3 種類があります。最後の 2 つは、同期イベントと呼ばれます。

Mutex オブジェクト

Mutex オブジェクトは同期オブジェクトであり、複数のスレッドで同時に所有することはできません。Mutex という名前は、Mutex オブジェクトの所有権が相互に排他的である事実に由来しています。スレッドは、リソースへの排他的アクセスが必要な場合に、Mutex オブジェクトの所有権を要求します。Mutex オブジェクトを所有できるスレッドは常に 1 つのみであるため、スレッドでリソースを使用するには Mutex オブジェクトの所有権を待つ必要があります。

WaitOne メソッドは、呼び出し側スレッドに Mutex オブジェクトの所有権を待たせます。スレッドが Mutex オブジェクトの所有権を持っている間に正常終了すると、Mutex オブジェクトのステータスは通知済みに設定され、待機中の次のスレッドに所有権が移ります。

同期イベント

同期イベントは、何かが発生したこと、あるいはリソースが使用可能であることを他のスレッドに通知するために使用します。イベントという言葉が使用されてはいますが、同期イベントは他の Visual Basic イベントとは異なり、実際には待ちハンドルです。他の待ちハンドルと同様、同期イベントには通知済みと未通知の 2 つの状態があります。同期イベントの待ちメソッドのいずれかを呼び出したスレッドは、他のスレッドが Set メソッドを呼び出してイベントに通知するまで待つ必要があります。これらの同期イベントには、2 つの同期イベント クラスがあります。スレッドは Set メソッドを使用して、ManualResetEvent インスタンスの状態を通知済みに設定します。またスレッドは、Reset メソッドを使用するか、待機中の WaitOne 呼び出しにコントロールが戻ったとき、ManualResetEvent インスタンスの状態を未通知に設定します。AutoResetEvent クラスのインスタンスも Set を使用して通知済みに設定できますが、イベントが通知済みになったという通知を待機中のスレッドが受け取るとすぐに、自動的に未通知に戻ります。

以下のコードは、AutoResetEvent クラスを使用してスレッドプール タスクを同期する例を示しています。

Sub StartTest()
   Dim AT As New AsyncTest()
   AT.StartTask()
End Sub

Class AsyncTest
   Private Shared AsyncOpDone As New _
      System.Threading.AutoResetEvent(False)

   Sub StartTask()
      Dim Tpool As System.Threading.ThreadPool
      Dim arg As String = "SomeArg"
      Tpool.QueueUserWorkItem(New System.Threading.WaitCallback( _
         AddressOf Task), arg)  ' タスクをキューに追加します。
      AsyncOpDone.WaitOne() ' スレッドが Set を呼び出すのを待ちます。
      MsgBox("スレッドが終了しました。")
   End Sub

   Sub Task(ByVal Arg As Object)
      MsgBox("スレッドを開始中です。")
      System.Threading.Thread.Sleep(4000) ' 4 秒待ちます。
      MsgBox("状態オブジェクトには文字列" & CStr(Arg) & "が含まれます。")
      AsyncOpDone.Set()   ' スレッドが終了したことを通知します。
   End Sub
End Class

モニタ オブジェクトと SyncLock

モニタ オブジェクトは、他のスレッドによって実行中のコードによって、コードのブロックが中断されないようにするため使用します。言い換えると、同期されたコード ブロックが終了するまで他のスレッドのコードは実行できません。SyncLock キーワードは、Visual Basic .NET でモニタ オブジェクトへのアクセスを簡素化するために使用します。Visual C#™ .NET では、Lock キーワードを同じ目的に使用します。

データを非同期的に繰り返し読み取り、その結果を表示するプログラムがあるとします。プリエンプティブなマルチタスキングを使用するオペレーティング システムでは、他のスレッドを実行するため、オペレーティング システムによってスレッドの実行が中断されることがあります。同期化を適用しないと、データ表示中にデータを表わすオブジェクトが別のスレッドによって変更され、一部分のみが更新されたデータ表示となる可能性があります。SyncLock ステートメントによって、コードのセクションが中断せずに実行されるようになります。以下の例では SyncLock を使用して、表示プロシージャにデータ オブジェクトへの排他的アクセスを与える方法を示しています。

Class DataObject
   Public ObjText As String
   Public ObjTimeStamp As Date
End Class

Sub RunTasks()
   Dim MyDataObject As New DataObject()
   ReadDataAsync(MyDataObject)
   SyncLock MyDataObject
      DisplayResults(MyDataObject)
   End SyncLock
End Sub

Sub ReadDataAsync(ByRef MyDataObject As DataObject)
   ' データを非同期に読み取り、処理するコードを追加します。
End Sub

Sub DisplayResults(ByVal MyDataObject As DataObject)
   ' 結果を表示するコードを追加します。
End Sub

コードのセクションが、別のスレッドで実行中のコードによって中断されないようにするには、SyncLock を使用します。

Interlocked クラス

Interlocked クラスのメソッドを使用すると、複数のスレッドが同時に同じ値を更新または比較しようとした場合に発生する問題を予防できます。このクラスのメソッドにより、どのスレッドの値でも安全に増加、減少、交換、および比較できます。以下の例では、別のスレッドで実行中のプロシージャと共有されている変数を増加するための、Increment メソッドの使用方法を示しています。

Sub ThreadA(ByRef IntA As Integer)
   System.Threading.Interlocked.Increment(IntA)
End Sub

Sub ThreadB(ByRef IntA As Integer)
   System.Threading.Interlocked.Increment(IntA)
End Sub

ReaderWriter ロック

場合によっては、データが書き込まれている間だけリソースをロックし、データを更新中でないときは複数のクライアントに同時にデータ読み取りを許可することも考えられます。ReaderWriterLock クラスは、スレッドがリソースを修正している間はリソースへの排他的アクセスを与え、リソース読み取り中は非排他的アクセスを与えます。ReaderWriter ロックは、排他的ロックに代わる便利な手法です。排他的ロックでは、データを更新する必要のないスレッドまで待機状態となりますが、ReadWriter ロックにはそのようなことがありません。以下の例では、複数のスレッドの読み取り処理と書き込み処理を適切に組み合わせるため、ReaderWriter を使用する方法を示しています。

Class ReadWrite
' ReadData メソッドと WriteData メソッドを複数のスレッドから安全に 
' 呼び出せます。
   Public ReadWriteLock As New System.Threading.ReaderWriterLock()
   Sub ReadData()
      ' このプロシージャは、いくつかのソースから情報を読み取ります。
      ' 読み取りロックでは、スレッドが読み取りを終了するまで、データへの 
      ' 書き込みができません。他のスレッドは ReadData を呼び出せます。
      ReadWriteLock.AcquireReaderLock(System.Threading.Timeout.Infinite)
      Try
         ' 読み取り処理をここで実行します。
      Finally
         ReadWriteLock.ReleaseReaderLock() ' 読み取りロックを解除します。
      End Try
   End Sub

   Sub WriteData()
      ' このプロシージャは、いくつかのソースに情報を書き込みます。
      ' 書き込みロックでは、スレッドが書き込みを終了するまで
      ' データの読み取りと書き込みができません。
      ReadWriteLock.AcquireWriterLock(System.Threading.Timeout.Infinite)
      Try
         ' 書き込み処理をここで実行します。
      Finally
         ReadWriteLock.ReleaseWriterLock() ' 書き込みロックを解除します。
      End Try
   End Sub
End Class

デッドロック

スレッド同期はマルチスレッド アプリケーションにおいて非常に重要ですが、複数のスレッドがお互いを待機するデッドロックの危険性が常にあります。これは交差点で自動車が立ち往生し、各自が相手の通過を待っている状態に似ています。つまり、デッドロックが発生すると、すべてが止まってしまいます。言うまでもなく、デッドロックを避けることは重要です。デッドロック状況に陥る道筋はいくつもあり、それと同じくらい多くの予防手段があります。この文書でデッドロックに関するすべての問題を論じる余裕はありませんが、慎重なプランニングが予防の鍵であることを指摘しておきます。コーディングを開始する前に、マルチスレッド アプリケーションをダイアグラム化することにより、多くの場合、デッドロックが発生する状況を予見できます。

スレッド タイマ

Threading.Timer クラスは、あるタスクを別々のスレッドで定期的に実行する場合に便利です。たとえばスレッド タイマを使用すると、データベースのステータスや整合性のチェック、重要なファイルのバックアップなどができます。以下の例では、あるタスクを 2 秒ごとに開始し、フラグを使用して、タイマを止める Dispose メソッドを起動しています。この例ではステータスを出力ウィンドウに送っているため、コードをテストする前に Ctrl + Alt + O キーを押してこのウィンドウを表示してください。

Class StateObjClass
' TimerTask 呼び出しのパラメータを保持するために使用します。
      Public SomeValue As Integer
      Public TimerReference As System.Threading.Timer
      Public TimerCanceled As Boolean
End Class

Sub RunTimer()
   Dim StateObj As New StateObjClass()
   StateObj.TimerCanceled = False
   StateObj.SomeValue = 1
   Dim TimerDelegate As New Threading.TimerCallback(AddressOf TimerTask)
   ' プロシージャを 2 秒ごとに呼び出すタイマを作成します。
   ' 注 : Start メソッドはありません。インスタンスが作成されるとすぐに 
   ' タイマの実行が開始されます。
   Dim TimerItem As New System.Threading.Timer(TimerDelegate, StateObj, _
                                               2000, 2000)
   StateObj.TimerReference = TimerItem  ' Dispose の参照を保存します。

   While StateObj.SomeValue < 10 ' ループを 10 回実行します。
      System.Threading.Thread.Sleep(1000)  ' 1 秒待ちます。
   End While

   StateObj.TimerCanceled = True  ' タイマ オブジェクトの Dispose を要求します。
End Sub

Sub TimerTask(ByVal StateObj As Object)
   Dim State As StateObjClass = CType(StateObj, StateObjClass)
   Dim x As Integer
   ' Interlocked クラスを使用してカウンタ変数を増加します。
   System.Threading.Interlocked.Increment(State.SomeValue)
   Debug.WriteLine("新規スレッドを開始しました。" & Now)
   If State.TimerCanceled Then    ' Dispose を要求しました。
      State.TimerReference.Dispose()
      Debug.WriteLine("終了しました : " & Now)
   End If
End Sub

コンソール アプリケーションを開発するときなど、System.Windows.Forms.Timer クラスを使用できない場合、スレッド タイマが特に便利になります。

タスクのキャンセル

マルチスレッドの利点の 1 つとして、タスクが別のスレッドで実行中の場合でも、アプリケーションのユーザー インターフェイス部分がアクティブであることが挙げられます。フラグとして機能するフィールドや同期イベントは、停止させる別のスレッドに通知するためによく使用されます。以下の例では、同期イベントを使用してタスクをキャンセルしています。この例を使用するには、以下のモジュールをプロジェクトに追加してください。スレッドを開始するには、StartCancel.StartTask() メソッドを呼び出します。実行スレッドを 1 つ以上キャンセルするには、StartCancel.CancelTask() メソッドを呼び出します。

Module StartCancel
   Public CancelThread As New System.Threading.ManualResetEvent(False)
   Public ThreadisCanceled As New System.Threading.ManualResetEvent(False)
   Private Sub SomeLongTask()
      Dim LoopCount As Integer
      Dim Loops As Integer = 10
      ' While ループで 10 秒経過するまで、または
      ' CancelThread が設定されるまでコードを実行します。
      While Not CancelThread.WaitOne(0, False) And LoopCount < Loops
         ' ここで何らかのタスクを実行します。
         System.Threading.Thread.Sleep(1000) ' 1 秒間スリープします。
              LoopCount += 1
      End While
      If CancelThread.WaitOne(0, False) Then
         ' ManualResetEvent CancelThread が設定されたことを通知します。
                ThreadisCanceled.Set()
         MsgBox("スレッドをキャンセルしています。")
      Else
         MsgBox("スレッドを終了しました。")
      End If
   End Sub

   Public Sub StartTask()
      ' 新規スレッドを開始します。
      Dim th As New System.Threading.Thread(AddressOf SomeLongTask)
      CancelThread.Reset()
      ThreadisCanceled.Reset()
      th.Start()
      MsgBox("スレッドを開始しました。")
   End Sub

   Public Sub CancelTask()
      ' StartTask プロシージャによって開始されたスレッドを停止します。
      ' このスレッドは、同期イベントの受信も送信も両方 
      ' 実行してスレッド間の調整を行います。
        CancelThread.Set()  ' CancelThread を設定してスレッドの中止を要求します。
      If ThreadisCanceled.WaitOne(4000, False) Then
         ' スレッドが中止を通知するまで、最大 
         ' 4 秒間待ちます。
         MsgBox("スレッドを中止しました。")
      Else
         MsgBox("スレッドを中止できません。")
      End If
   End Sub
End Module

まとめ

マルチスレッド処理は、スケーラブルで高い応答性を持つアプリケーションへの鍵となります。Visual Basic .NET では、強力なマルチスレッド開発モデルをサポートしており、開発者がマルチスレッド アプリケーションの能力をすばやく引き出すことが可能になっています。

  • Visual Basic .NET では新しい .NET Framework クラスを採用し、マルチスレッド アプリケーションを容易に作成できます。
  • 複数のスレッドによってパフォーマンスを向上することができますが、スレッドを作成するために必要なメモリや、実行するためのプロセッサ時間など、各スレッドにはコストもかかることを忘れないでください。
  • スレッドのプロパティとメソッドは、スレッド間の相互動作を制御し、スレッド実行にリソースがいつ利用できるかを決定します。
  • マルチスレッドは混乱を招くように見えるかもしれませんが、同期テクニックを使用することによって実行スレッドを制御できます。
  • マルチスレッドでは、利用可能なリソースを効率的に配分することにより、アプリケーションが複雑になったとしてもスケーラブルなアプリケーションを作成できます。

この文書で説明したテクニックを活用することにより、プロセッサを集中的に使用するタスクでも処理できる、実用性の高いアプリケーションを開発できます。

その他のリソース