Visual Studio 2005 による応答性の高いアプリケーション構築のためのスレッド使用
Brad McCabe
June 2005
日本語版最終更新日 2005 年 12 月 27 日
適用対象:
Visual Basic 2005
Visual Studio 2005
概要: Visual Studio 2005 の BackgroundWorker を使用することにより、マルチスレッド アプリケーションの構築が簡単になります。また、少ない作業で対話式のアプリケーションを作成することもできます。
関連のサンプル コード VS05ThreadingSampleCode.msi をダウンロードしてください。
目次
はじめに 長時間実行タスクの例 BackgroundWorker を使用したマルチスレッドの例 まとめ
はじめに
Visual Basic .NET に導入されている最も強力な機能の 1 つとして、マルチスレッド アプリケーションの作成があります。Visual Basic 開発者の多くは、マルチスレッド アプリケーションの開発が複雑で難解であるため、この新しく得た機能をあまり利用しませんでした。
Visual Basic 2005 ではこれがどのように簡単になっているかを説明する前に、まずは、開発者がよく直面する課題として、実行中のユーザーの入力や操作がシステムで制限される長時間実行タスクを考えてみましょう。
長時間実行タスクの例
この例では、特定の整数のフィボナッチ数列を計算する長時間実行タスクを取り上げます。開発者がアプリケーションの中でこの計算を行うことはあまりないでしょう。しかし、データベースや他のよく使用されるインフラストラクチャを必要とせずに実行できる例としては最適です。実際のアプリケーションの中で行う長時間実行タスクは、負荷の高いデータベース操作、従来システムの呼び出し、または外部サービスや、リソースを大量に消費する操作の呼び出しなどになります。
プロジェクトの作成は、Windows フォーム アプリケーションの新規作成から始めます。フォームには、プログレス バー、2 つのボタン、数字入力ボックス、および結果を表示するラベルを配置します。ボタンにはそれぞれ、startSyncButton および cancelSyncButton という名前を付け、ラベルの text は (no result) に設定します。これらを配置すると、フォームは次のようになります。

図 1. 新しく作成した Windows フォーム アプリケーション
このフォームに、フィボナッチ数列を計算するコードを追加します。
Function ComputeFibonacci(ByVal n As Integer) As Long
' パラメータ n は >= 0 かつ <= 91 でなければならない
' Fib(n) は n > 91 であると long がオーバーフローする
If n < 0 OrElse n > 91 Then
Throw New ArgumentException( _
"value must be >= 0 and <= 91", "n")
End If
Dim result As Long = 0
If n < 2 Then
result = 1
Else
result = ComputeFibonacci(n - 1) + _
ComputeFibonacci(n - 2)
End If
' 全タスクのパーセンテージで進行状況を表示する
Dim percentComplete As Integer = CSng(n) / _
CSng(numberToCompute) * 100
If percentComplete > highestPercentageReached Then
highestPercentageReached = percentComplete
Me.ProgressBar1.Value = percentComplete
End If
Return result
End Function
このコードはとても単純です。これは、再帰的に自分を呼び出すことにより値を計算します。これは数が小さい場合の実行は速くても、大きな数を入力するとパフォーマンスが急速に低下します。
この関数は、コードが 1 回処理されるごとにプログレス バーを更新し、ユーザーに進行状況と、アプリケーションがまだ実行中であることを示します。
では、開始ボタンにコードを追加して、この関数を実行するようにします。
Private Sub startSyncButton_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles startSyncButton.Click
' 結果ラベルの text をリセットする
result.Text = [String].Empty
' 同期動作が完了するまで
' UpDown コントロールを使用不可にする
Me.numericUpDown1.Enabled = False
' 同期動作が完了するまで
' Start ボタンを使用不可にする
Me.startSyncButton.Enabled = False
' 同期動作が実行中は
' Cancel ボタンを使用可能にする
Me.cancelSyncButton.Enabled = True
' UpDown コントロールから値を取得し
' グローバル変数 numberToCompute に保存する
numberToCompute = CInt(numericUpDown1.Value)
' パーセンテージを保持する変数をリセットする
highestPercentageReached = 0
' 同期動作を開始する
result.Text = ComputeFibonacci(numberToCompute).ToString
' UpDown コントロールを使用可能にする
Me.numericUpDown1.Enabled = True
' Start ボタンを使用可能にする
startSyncButton.Enabled = True
' Cancel ボタンを使用不可にする
cancelSyncButton.Enabled = False
End Sub
このコードは、ごく一般的なものです。世界中で多くのアプリケーションがこのように設計されています。ユーザーがボタンをクリックするとフィボナッチの値が計算され、画面に表示されます。しかし、この設計には大きな問題があります。
ボタンが押されたときに、メイン スレッドは、UI からの要求を処理できるよう応答可能でなければなりませんが、フィボナッチの値の計算でビジーでもあります。アプリケーションを開始し大きな数字 (たとえば 50 など) を入力すると、この問題が発生することを確認できます。Start ボタンをクリックした後、アプリケーションを最小化したり、ウィンドウを移動したりしてみてください。実行中、アプリケーションは後からのユーザーの情報に応答しないか、または非常に遅く応答します。

図 2. 関数は実行中であってもアプリケーションが応答しないように見える
遅い、または応答しないということだけでなく、この場合、ユーザーはプロセスをキャンセルすることができません。ユーザーは、間違えて大きな数字を入力してしまった場合、または処理を待つことができない場合にはどうしたらよいでしょうか。
これを確認するために、Cancel ボタンに以下のサンプルのコードを設定します。
Private Sub cancelSyncButton_Click(ByVal sender As System.Object,_
ByVal e As System.EventArgs) Handles cancelSyncButton.Click
MsgBox("Cancel")
End Sub
これは、キャンセルが要求されたことを示すメッセージ ボックスを表示する基本的なコードです。アプリケーションを実行し、再度大きな数字を入力して、Cancel ボタンをクリックしてみます。しかし、プロセスが実行中は何も起こりません。
これを解決する最も良い方法は、長時間実行プロセスを別のスレッドに移動することです。これにより、メイン スレッドをユーザーの入力に応答できるように空けておくことができるので、アプリケーションがユーザーに応答できるように保たれます。
BackgroundWorker を使用したマルチスレッドの例
Visual Basic 6.0 では、問題を回避するために、タイマーなどのいくつかの細工をせずに新しいスレッドを作成することはできませんでした。Visual Basic .NET では、これが簡単になりました。新しいスレッド オブジェクトを作成し、実行するメソッドを渡して、スレッド オブジェクトの start メソッドを呼び出します。
Dim myThread As New Thread(AddressOf MyFunction)
myThread.Start()
しかし、応答性を高めるためには、相当な作業が必要です。Visual Basic 6.0 では、新しいスレッドの作成、および開始は簡単になりましたが、アプリケーションが適切に設計されるように注意しなければならず、多くのバグや問題の危険がありました。
適切な設計の複雑性のために、多くの Visual Basic 開発者はマルチスレッドをあまり採用しませんでした。Visual Basic 2005 では、新しい BackgroundWorker コンポーネントによって、この作業がより簡単で、安全になっています。
簡単にアプリケーションをマルチスレッド化し、ユーザーに対する応答性を高める方法を紹介します。まず、上記のフォームと同じレイアウトとコードで MultiThreaded という新しいフォームを作成します。ただし、今回は、ボタンの名前をそれぞれ、startAsyncButton、および cancelAsyncButton とします。これは、今回は、メイン スレッドをブロックせずに、コードを非同期 (asynchronous) に実行するためです。
新しいフォームではまず、フォームをデザイン モードで開き、ツールボックスの Components セクションから BackgroundWorker コンポーネントをフォームにドラッグ アンド ドロップします。コンポーネントのプロパティ ウィンドウ で、WorkerReportProgress プロパティ、およびWorkerSupportsCancellation プロパティを true に設定します。これらの設定により、後で説明するように、プログレス バーの更新、および処理の停止が可能になります。

図 3. 新しい BackgroundWorker コンポーネントにより非常に簡単になるマルチスレッド アプリケーションの作成
ここまで完了したら、Start ボタンのコードを以下のように変更します。
Private Sub startAsyncButton_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles startAsyncButton.Click
' 結果ラベルの text をリセットする
result.Text = [String].Empty
' 非同期動作が完了するまで
' UpDown コントロールを使用不可にする
Me.numericUpDown1.Enabled = False
' 非同期動作が完了するまで
' Start ボタンを使用不可にする
Me.startAsyncButton.Enabled = False
' 非同期動作が実行中は
' Cancel ボタンを使用可能にする
Me.cancelAsyncButton.Enabled = True
' UpDown コントロールから値を取得する
numberToCompute = CInt(numericUpDown1.Value)
' パーセンテージを保持する変数をリセットする
highestPercentageReached = 0
' 非同期動作を開始する
backgroundWorker1.RunWorkerAsync(numberToCompute)
End Sub
BackgroundWorker コンポーネントの RunWorkerAsync メソッドを呼び出し、このメソッド呼び出し以降のコードをすべて削除していることに注意してください。
別のスレッドでコードを呼び出す場合は、RunWorkerAsync メソッドを呼び出します。これにより、BackgroundWorker の DoWork というイベントが生成されます。このイベントで、フィボナッチの値を計算します。
' このイベント ハンドラで実際の処理を行う
Private Sub backgroundWorker1_DoWork( _
ByVal sender As Object, _
ByVal e As DoWorkEventArgs) _
Handles backgroundWorker1.DoWork
' このイベントを発生させた BackgroundWorker オブジェクトを取得
Dim worker As System.ComponentModel.BackgroundWorker= _
CType(sender, System.ComponentModel.BackgroundWorker)
' 計算の結果を
' DoWorkEventArgs オブジェクトの Result プロパティに割り当てる
' これは
' RunWorkerCompleted イベント ハンドラで使用可能
e.Result = ComputeFibonacci(e.Argument, worker, e)
End Sub
今回は、非同期イベントを処理するコードを直接ここに設定していますが、実際の多くの場合には別のプロシージャに分けておくことをお勧めします。この例では、既存の ComputeFibonacci 関数を少し修正して使用します。
Function ComputeFibonacci( _
ByVal n As Integer, _
ByVal worker As System.ComponentModel.BackgroundWorker, _
ByVal e As System.ComponentModel.DoWorkEventArgs) As Long
' パラメータ n は >= 0 かつ <= 91 でなければならない
' Fib(n) は n > 91 であると long がオーバーフローする
If n < 0 OrElse n > 91 Then
Throw New ArgumentException( _
"value must be >= 0 and <= 91", "n")
End If
Dim result As Long = 0
' ユーザーがキャンセルした場合、処理は停止します。
' CancelAsync の呼び出しは、
' このメソッドの最後の実行が終了した直後に
' CancellationPending を true に設定します。
' このため、このコードでは、
' DoWorkEventArgs.Cancel フラグが true になりません。
' つまり、RunWorkerCompletedEventArgs.Cancelled は、
' RunWorkerCompleted イベント ハンドラでは true に設定されていません。
' これは、競合する状態です。
If worker.CancellationPending Then
e.Cancel = True
Else
If n < 2 Then
result = 1
Else
result = ComputeFibonacci(n - 1, worker, e) + _
ComputeFibonacci(n - 2, worker, e)
End If
' 全タスクのパーセンテージで進行状況を表示する
Dim percentComplete As Integer = _
CSng(n) / CSng(numberToCompute) * 100
If percentComplete > highestPercentageReached Then
highestPercentageReached = percentComplete
worker.ReportProgress(percentComplete)
End If
End If
Return result
End Function
Visual Basic でマルチスレッド アプリケーションを扱う場合には、メイン UI スレッドなどの別のスレッドに作成されたコンポーネント、およびオブジェクトへのアクセスでよくエラーが発生します。これは、すべてのオブジェクトがスレッド セーフではないため、アプリケーションが予期しない誤作動をすることにもなります。
計算する関数には BackgroundWorker、およびイベント引数を渡すことに注意してください。これにより、プログレス バーなどのコントロールの更新は、処理中のプロセスからではなく、メイン スレッドに戻ってイベントを発生させて行うことができます。
ComputeFibonacci 関数の各実行の後に、BackgroundWorker コンポーネントの ReportProgress メソッドを呼び出します。これにより、このコンポーネントの ProgressChanged が発生します。このイベントは、メイン UI スレッドに戻って発生し、ProgressBar を更新します。クロス スレッドの例外を避けるために、新しいスレッドでは ProgressBar を更新しません。
このイベントのコードは簡単です。次に示します。
Private Sub backgroundWorker1_ProgressChanged( _
ByVal sender As Object, ByVal e As ProgressChangedEventArgs) _
Handles backgroundWorker1.ProgressChanged
Me.progressBar1.Value = e.ProgressPercentage
End Sub
新しいフォームをコンパイルし、実行してみます。50 というような大きな数字を入力して、フォームを最小化あるいは最大化したり、移動したりしても、前回のように問題や遅れが生じないことが確認できます。これは、第 2 のスレッドがフィボナッチの値を計算し、メイン UI スレッドはこれらのユーザーの要求を処理することが可能であるためです。
ユーザーの入力に応答できるようにメイン スレッドを空けることができたので、Cancel ボタンにコードを設定し、ユーザーが処理を停止する機能を追加します。
Private Sub cancelAsyncButton_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles cancelAsyncButton.Click
' 非同期動作をキャンセル
Me.backgroundWorker1.CancelAsync()
' Cancel ボタンを使用不可にする
cancelAsyncButton.Enabled = False
End Sub
バックグランド プロセスを停止するために、単純に、BackgroundWorker の CancelAsync メソッドを呼び出していることに注意してください。この呼び出しは、コンポーネントの CancellationPending プロパティ を true に設定します。このプロパティは、ComputeFibonacci 関数の各実行でチェックしています。
関数が処理を完了すると、BackgroundWorker は RunWorkerCompleted イベントを発生させます。このイベントには、ユーザーに結果を表示し、画面のボタンをリセットし次の操作に備えるためのコードをすべて設定します。
Private Sub backgroundWorker1_RunWorkerCompleted( _
ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) _
Handles backgroundWorker1.RunWorkerCompleted
' 最初に、例外がスローされた場合を処理
If Not (e.Error Is Nothing) Then
MessageBox.Show(e.Error.Message)
ElseIf e.Cancelled Then
' 次に、ユーザーが計算をキャンセルした場合を
' 処理
result.Text = "Canceled"
Else
' 最後に、計算が正常に完了した場合を処理
result.Text = e.Result.ToString()
End If
' UpDown コントロールを使用可能にする
Me.numericUpDown1.Enabled = True
' Start ボタンを使用可能にする
startAsyncButton.Enabled = True
' Cancel ボタンを使用不可にする
cancelAsyncButton.Enabled = False
End Sub
まとめ
上記のサンプルでは、Visual Basic 2005 で BackgroupWorker コンポーネントとそのプロパティ、メソッド、およびイベントを使用することにより、マルチスレッド アプリケーションの処理が簡単になることを説明しました。わずかな変更で、ユーザーがタスクをキャンセル、または停止できるだけでなく、より対話性の高い Windows フォーム アプリケーションを作成するようにコードを修正できました。パフォーマンスおよび対話性に最も優れたアプリケーションをユーザーに提供するには、大規模な、または長時間実行のタスクをメイン UI スレッドから外すことを目的とするマルチスレッド設計の採用を検討することをお勧めします。
|