ステップ 7 ハンズオン : 非同期処理の注意点
Windows フォームにおけるパフォーマンスの向上 (非同期処理編)
マイクロソフト株式会社 デベロッパーマーケティング本部
デベロッパーエバンジェリスト 高橋 忍
最終更新日 2005 年 3 月 16 日
非同期処理を使ったパフォーマンス テスト アプリケーションの作成
では、delegate を使った非同期処理によるパフォーマンスの違いを確認するためのテスト アプリケーションを修正します。

Visual Studio .NET 2003 を起動して、作成したプロジェクトを開きます。ここに checkBox コントロールを 1 つ追加します。
| コントロール ID | プロパティ名 | 設定値 |
| CheckBox1 | (Name) | chkID |
Text | ID |

図 1. アプリケーション フォーム

ではまずは、スレッドの状態がどのようになっているか確認してみましょう。リストボックスにスレッドの ID を書き込むように修正します。Additem メソッドに以下のコードを追加します。
Private Sub additem(ByVal s As String)
If Me.chkID.Checked Then
Me.ListBox1.Items.Add(System.AppDomain.GetCurrentThreadId & ":" & s)
Else
Me.ListBox1.Items.Add(s)
End If
End Sub
スレッドにはそれぞれスレッド ID という固有の番号がつけれられています。自身のスレッド ID は AppDomain クラスの GetCurrentThredID メソッドで取得することができます。Additem メソッドはこれを呼び出すスレッド上で実行されるため、additem メソッドを呼び出したスレッドの ID を取得することができます。

では実行して状態を確認してみましょう。アプリケーションを実行したら、非同期処理のチェックボックスを選択し、ID チェックボックスにチェックを入れます。準備ができたら開始ボタンを押します。今度は記録されるメッセージの前に処理を行ったスレッドの ID が記述されます。

図 2. 記録されるメッセージの前に処理を行ったスレッドの ID が記述される
この ID はアプリケーションを実行するごとに変化します。ただし共通点として、start だけが違う ID となっており、それ以降の処理はすべて同じプロセス ID となっています。このことから、start を記述したスレッドはこのアプリケーションのスレッドです。ですから業務処理開始以降はアプリケーションのスレッドとは別のスレッドで実行されていることがわかります。
非同期処理の時にも説明しましたが、Windows アプリケーションには 「window に対する操作はウィンドウを生成したスレッドから行わなければならない」という原則があります。それに対して、現在は業務処理開始以降はウィンドウとは別のスレッドからリストボックスに書き込む処理を行っています。一見正しく動いているように見えますが、その動作は保障されたものではありません。そのためこの部分を修正してやる必要があります。

では、ここで修正のための検討をします。とは言ってもこの処理はすでにパターン化されている処理ですので、どのような仕組みになっていてどのような動きになるのか、理解するのがよいでしょう。
[現在のスレッドがウィンドウのスレッドか判定する]
- ウィンドウのスレッドであれば
- ウィンドウのスレッドでない
>> ウィンドウのスレッドから additem を呼び出す
ちょっとややこしいかもしれません。ウィンドウとは異なるスレッドから additem を呼び出していた場合、ウィンドウのスレッドから additem を呼び出します。そしてもう一度スレッドの判定をしてようやくリストボックスに追加処理をします。ちょっと再起呼び出しにも似ていますね。

ではまず、スレッドの判定処理から追加します。ウィンドウのスレッドかどうかの判定にはうってつけのプロパティがあります。それが、コントロールオブジェクトの InvokeRequired プロパティです。
これは呼び出し元のスレッドがコントロール (ウィンドウ) のスレッドかどうかの判定に使えるプロパティです。このプロパティが true の場合、別スレッドで実行されていることになります。フォームの InvokeRequired プロパティを判定することで、フォームと実行しているスレッドが異なるスレッドか判別することができるわけです。
そこで、additem に以下のようなコードを追加して、判定処理を実装します。
Private Sub additem(ByVal s As String)
If Me.InvokeRequired Then
'ウィンドウスレッドから実行するための処理
Else
If Me.chkID.Checked Then
Me.ListBox1.Items.Add(System.AppDomain.GetCurrentThreadId & ":" & s)
Else
Me.ListBox1.Items.Add(s)
End If
End If
End Sub

さて、次に必要なのが、ウィンドウのスレッドからメソッドを実行するための処理です。別スレッドからメソッドを実行するには特別な処理をしてやらなくてはなりません。(そのまま呼び出してもスレッドは変わりません) 別スレッドから処理を実行できるようにするマーシャリングと呼ばれる処理をします。実は、これは前回使ったデリゲートを使って処理を行います。Additem に紐づいたデリゲートインスタンスを準備して処理を行います。
前回は別スレッドで実行するために BeginInvoke メソッドを使用しました。ここではフォームと同じメソッドで実行することが目的なので、フォームの Invoke メソッドを使用します。フォームの Invoke メソッドはフォームのスレッド上でデリゲートを呼び出します。このため安全にフォーム上のコントロールの処理を行うことができるようになります。
ではまず、デリゲートの準備から入ります。前回と同じようにデリゲートの定義をします。デリゲートから呼び出す additem メソッドには string 型の引数がありますので、同様に定義します。以下のコードを追加しましょう。
Private Delegate Sub mydelegate()
Private Delegate Sub myitemdel(ByVal s As String)
続けてデリゲートインスタンスの作成から呼び出しまでの処理を追加します。以下のようにデリゲートインスタンスの作成をして、フォーム (Me) の Invoke メソッドを実行します。引数にはデリゲートインスタンスと、引数を渡すためのオブジェクトと引数を渡します。これでデリゲートを使って additem メソッドを実行する際に引数 (s) がきちんと渡されるようになります。
Private Sub additem(ByVal s As String)
If Me.InvokeRequired Then
'ウィンドウスレッドから実行するための処理
Dim dlgIAddItem As New myitemdel(AddressOf additem)
Me.Invoke(dlgAddItem, New Object() {s})
Else
If Me.chkID.Checked Then
Me.ListBox1.Items.Add(System.AppDomain.GetCurrentThreadId & ":" & s)
Else
Me.ListBox1.Items.Add(s)
End If
End If
End Sub

以上で、アプリケーションの実装は完了です。では実際にビルトしてパフォーマンスのテストをしてみましょう。「ビルト」メニューから「ソリューションのビルト」を実行してアプリケーションをビルトします。問題がなければ、「デバッグ」メニューから「デバッグ無しで開始」を実行します。
アプリケーションが起動したら、こんどは非同期処理のラジオボタンを選択して ID のチェックを入れてから「開始」ボタンを押してみましょう。今度は処理の開始から同じウィンドウのスレッド ID でリストボックスへの書き込みが行われていることがわかります。これでアプリケーションの安全性が保障されるようになりました。

図 3. マーシャリングによってウィンドウスレッドから処理が行われる
今回の処理は、少しわかりにくい部分もあったかもしれません。しかしこれはある程度場パターン化された処理ともいえますので、いくつかサンプルアプリケーションを試しながら、慣れていくのが早道でしょう。
|