Part 4. Visual Studio によるマルチスレッドアプリの開発


さて、Part 1~3 の解説で、Windows フォームにおけるマルチスレッドアプリケーションをスクラッチで開発する方法について述べてきました。結論としては、実は Windows フォームにおけるマルチスレッドアプリケーション開発は恐ろしく厄介で面倒である、ということになると思うのですが;、とはいえ

  • 長時間を要する処理があるため、どうしてもマルチスレッドアプリにしなければならない。

ということも当然あると思います。幸い、.NET Framework 2.0/Visual Studio 2005 以降では、BackgroundWorker コンポーネントをはじめとして、マルチスレッドアプリを比較的簡単に書けるようにするための各種のコンポーネントやツールセットがいくつか追加されました。最後にこれらについて解説して、4 回にわたるエントリを締めくくっていきたいと思います。

今回のエントリで解説する内容は以下の 3 つです。

  • XML Web サービス呼び出しの非同期処理化
  • WCF サービス呼び出しの非同期処理化
  • BackgroundWorker コンポーネントによる一般的なタスクの非同期処理化

なお、本エントリでは基本的な XML Web サービスの作り方・使い方に関する解説は行いません。*.asmx による XML Web サービス開発をご存じない方は、一般的な書籍や Web の情報などを参照してみてください。また、今回のサンプルコードはこちらになります。

では、順番に解説していきましょう。

[XML Web サービス呼び出しの非同期処理化]

ASP.NET 2.0 XML Web サービス(*.asmx)に対する Web サービス参照(.NET Framework 2.0 ベースのプロキシクラス)には、非常に簡単に XML Web サービス呼び出しを非同期処理化できる機能が備わっています。ここではこの機能を使って、長時間を要する XML Web サービス呼び出しを行う Windows フォームアプリケーションを開発してみます。

image

まず、新規に Windows フォームアプリケーションを作成し、そこに Web サイトプロジェクトを追加します。

image

次に、*.asmx ファイルを使って XML Web サービスを作ります。長時間呼び出しをシミュレートしたいので、Thread.Sleep() 命令を使って 5,000msec だけ待機するように実装しておきます。

   1: <%@ WebService Language="C#" Class="WebService" %>
   2:  
   3: using System;
   4: using System.Web;
   5: using System.Web.Services;
   6: using System.Web.Services.Protocols;
   7:  
   8: [WebService(Namespace = "http://tempuri.org/")]
   9: [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  10: public class WebService  : System.Web.Services.WebService {
  11:  
  12:     [WebMethod]
  13:     public string GetMessage(string name) {
  14:         System.Threading.Thread.Sleep(5000);
  15:         return "Hello World " + name;
  16:     }
  17: }
  18:  

実装が終わったら、Windows フォームアプリケーション側で XML Web サービス参照を作成します。なお、Visual Studio 2008 を利用している場合、既定では .NET Framework 3.0 ベースの WCF サービスプロキシが作成されてしまいます。このため、「サービス参照の追加」→「詳細設定」→「Web 参照の追加」を選択しして、.NET Framework 2.0 ベースのプロキシクラスを作成する画面を表示し、ここでプロキシクラスを作成してください。

image

プロキシクラスを作成したら、ボタンやテキストボックスなどを貼り付けて画面を作成し、いったんコンパイルを行います。すると、ツールボックス上に、XML Web サービスプロキシのクラスが現れますので、これを当該画面上に貼り付けます

image

そののち、以下の 2 つのイベントハンドラを記述します。

  • button1_Click イベントハンドラ

    ボタンが押されたときに発生するイベントハンドラ。画面に貼り付けた Web サービスプロキシ(webService1)を使って、XML Web サービスの非同期呼び出しを開始させる。
  • webService1_GetMessageCompleted イベントハンドラ

    非同期呼び出しが終了した場合に呼び出されるイベントハンドラ。ここに、XML Web サービス呼び出しが終わったときの処理(画面表示など)を書く。

※ 後者のイベントハンドラについては、プロパティ画面の上の方にあるイナズママークをクリックした上で、プロパティ画面内の「GetMessageCompleted」の項目をダブルクリックすると、作成することができます。

   1: private void button1_Click(object sender, EventArgs e)
   2: {
   3:     // 二重押し防止のためのコード
   4:     button1.Enabled = false;
   5:     textBox1.Enabled = false;   
   6:     // XML Web サービスの非同期呼び出し
   7:     webService1.GetMessageAsync(textBox1.Text);
   8: }
   9:  
  10: private void webService1_GetMessageCompleted(object sender, WindowsFormsApplication1.localhost.GetMessageCompletedEventArgs e)
  11: {
  12:     // 終了結果取り出し(XML Web サービス呼び出し中に例外が発生した場合には、e.Resultプロパティにアクセスした際に例外が発生)
  13:     string result = e.Result;
  14:     // 結果表示
  15:     label1.Text = result;
  16:     button1.Enabled = true;
  17:     textBox1.Enabled = true;
  18: }

さらに、Program.cs ファイルに集約例外ハンドラを記述します。

   1: static class Program
   2: {
   3:     [STAThread]
   4:     static void Main()
   5:     {
   6:         Application.EnableVisualStyles();
   7:         Application.SetCompatibleTextRenderingDefault(false);
   8:  
   9:         Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
  10:         Application.Run(new Form1());
  11:     }
  12:  
  13:     static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
  14:     {
  15:         MessageBox.Show("システムエラーが発生しました。アプリケーションを終了します。");
  16:         // 通常は例外ロギングも併せて実施
  17:         Application.Exit();
  18:     }
  19: }

以上で作業は終了です。実行してみて、以下のような挙動をすることを確認してみてください。

  • XML Web サービスの呼び出し中に、ウィンドウがフリーズしない(きちんとドラッグできる)。
  • アプリケーション起動後に ASP.NET 開発サーバを終了させて、ボタンを押下すると、ちゃんと集約例外ハンドラがフックされる。

image

image ← XML Web サービスを呼び出せなかった場合 

内部動作の概念図を下に示します。この処理のキーポイントは、UI スレッドへの戻りが自動的に行われる、という点です。webService1.GetMessageAsync() メソッドにより、Web サービス呼び出し自体は背後のスレッド(具体的にはプールスレッド)上で行われますが、

  • 呼び出しが正常終了した場合に呼び出される webService1_GetMessageCompleted() イベントハンドラは、UI スレッド上で呼び出される。このため、このイベントハンドラ内では自由に UI コントロールを操作してよい
  • 呼び出しが正常終了せず例外が発生した場合でも、webService1_GetMessageCompleted() イベントハンドラが UI スレッド上で呼び出される。そして、e.Result プロパティにアクセスした瞬間に、発生した例外がリスローされる

という挙動をします。

image

この挙動の中でも後者は非常に上手いところで、このような機能があるため、特に追加のコードを書かなくても、XML Web サービス呼び出し中に発生した例外を、Application.ThreadException 集約例外ハンドラで捕捉することができます。よって、上記のようなコードだけで、XML Web サービス呼び出しを非同期化することができる(背後のタスクスレッド上で動かすことができる)のです。

※ (注意) ただし、この実装方法では、XML Web サービス呼び出しをキャンセルすることはできません。一応 XML Web サービスプロキシには .CancelAsync() というメソッドがあるものの、これは「まだ未送信状態だったら呼び出しを取り消す」というものです。このため、実際にタスクスレッドで XML Web サービス呼び出しが行われてしまった後に .CancelAsync() したところで、行われてしまった呼び出しは取り消せません(=確実な呼び出し取り消しができるメソッドではありません)。もともとこの問題は、タスクスレッドを使っている以上は原理的に発生するものなので、設計時に注意しておくことが必要です。

※ (注意&参考) また、本題からは若干それますが、プロキシクラスを画面に貼り付けて利用する場合は、URL プロパティを構成設定ファイルから自動的に読み取らなくなってしまうため、下図のようにして明示的に紐付けを行ってください。(プロキシクラスのコード生成ツールとの兼ね合いで発生するトラブルのようです。明示的に紐付けすればきちんと読み取るようになります。)

image

[WCF サービス呼び出しの非同期処理化]

では今度は、同じことを .NET Framework 3.0 ベースの WCF プロキシクラスで行ってみましょう。話を簡単にするために、サーバ側は上記のサンプルと同じく、*.asmx を使うことにして、クライアント側に(サービス参照の追加機能を利用して) WCF のプロキシクラスを作成します。

image

作成したプロキシクラスは(先と異なり)フォーム上に貼り付けることはできません。しかし、以下のようなコードを書くことで、先ほどと同じようにコーディングすることができます。

image

このように、WCF プロキシクラスの場合には、画面上に貼り付けることはできないものの、きちんと UI スレッド上で呼び出し終了イベントハンドラを呼び出してもらうことができます。

※ (注意) .NET Framework 2.0 ベースの ASP.NET XML Web サービスプロキシの場合には、画面上に貼り付けなければなりません。コード上で Completed イベントハンドラの登録を行うと、UI スレッドへの戻りが発生しないため、注意してください。

さて、ここまで Web サービス呼び出しを非同期化する方法について解説してきましたが、最後に、より一般的なタスクを簡単に非同期化する方法について解説します。

[BackgroundWorker コンポーネントによる一般的なタスクの非同期処理化]

ここまでの解説からわかるように、Windows フォームにおけるマルチスレッドアプリケーションの難しさは、UI スレッドとタスクスレッド間での連携によるところが大きいです。この連携処理を簡素化するために .NET Framework 2.0 で導入されたのが、ここで解説する BackgroundWorker コンポーネントです。この BackgroundWorker コンポーネントは、UI スレッドとタスクスレッド(プールスレッド)との間の協調連携動作を支援するコンポーネントとして機能します。概念図を下に示します。

image

この概念図だけだとわかりにくいと思いますので、実際に BackgroundWorker コンポーネントを使って、長時間処理を背後で行う以下のようなアプリケーションを作ってみることにしたいと思います。

image

具体的な実装手順は、以下の通りです。(何をやっているのかをわかりやすく示すため、Step by Step で実装していきます。)

① UI の作成

  • まずは画面上に 2 つのボタン、ラベル、プログレスバーを置きます。
  • それぞれのボタンに、btnStart, btnCancel と名前をつけ、キャンセルボタンの Enable プロパティを false にしておきます。
  • 画面上に、BackgroundWorker コンポーネントを貼り付けます。

image

② 長時間処理の作成

  • btnStart_Click() イベントハンドラを作り、ここに、BackgroundWorker コンポーネントに対して非同期処理を開始する指示を出すコードを記述します。
  • 次に、backgroundWorker1_DoWork() イベントハンドラを作り、ここに実際の長時間処理を記述します。
  • 最後に、backgroundWorker1_RunWorkerCompleted() イベントハンドラを作り、ここに終了後の処理を記述します。

実際の処理の流れを以下に示します。重要なのは、UI スレッド → プールスレッド → UI スレッドの流れが自動的に制御される、という点です。従来だと、自力で .BeginInvoke() などを記述しなければなりませんでしたが、そうした処理はすべて BackgroundWorker が肩代わりしてくれます。

image

③ 起動パラメータと処理結果の引き渡し

さて、上記のサンプルだと、タスクスレッドの起動パラメータの受け渡しや、タスクスレッドの処理結果の受け取りがありません。これらのコードを追加すると、以下のようになります。

image

image (30msec × 321 回なので 10 秒ぐらいかかります)

④ 進捗状態表示機能の追加

では次に、進捗状態を UI 上に表示する機能を追加します。進捗状態は、プールスレッドから UI スレッドへの通知が必要ですが、これを行うために、以下の 2 つの作業を行います。

  • backgroundWorker1 の WorkerReportsProgress プロパティを true に変更する。
  • backgroundWorker1_DoWork() メソッドの中に、進捗報告のためのコードを追加する。(backgroundWorker1.ReportProgress() メソッド)
  • backgroundWorker1_ProgressChanged() イベントハンドラを追加し、UI に表示する。

image

このようにすると、進捗状態が UI に表示されるようになります。

image

ここで注意していただきたいのは、プールスレッドで動作している backgroundWorker1_DoWork() メソッドから、UI 更新を行う backgroundWorker1_ProgressChanged() メソッドを直接呼び出しているわけではない、という点です。

  • プールスレッドからは、backgroundWorker1 の .ReportProgress() メソッドを叩き、backgroundWorker1 にスレッド同期を依頼する。
  • backgroundWorker1 は、UI スレッド上で backgroundWorker1_ProgressChanged イベントハンドラを呼び出すように、内部で .BeginInvoke() 命令を利用する。

ここでもう一度、最初に示した内部動作の模式図を示します。

image

最初からの流れをもう一度追いかけてみると、

  • BackgroundWorker コンポーネントを用いたタスクスレッドの起動

    ① UI スレッドから BackgroundWorker コンポーネントの RunWorkerAsync() を叩く

    ② BackgroundWorker が自動的にプールスレッドに処理開始要求を投入する

    ③ その結果、DoWork イベントハンドラに登録されたメソッド(backgroundWorker1_DoWork() メソッド)が、プールスレッド上で起動する

  • BackgroundWorker コンポーネントを用いた進捗状況の UI への通知

    ① プールスレッドから適当なタイミング(なるべく頻繁に)で BackgroundWorker コンポーネントの ReportProgress() メソッドを叩く

    ② BackgroundWorker は、内部で .BeginInvoke() を行い、メッセージキューにメッセージを投入

    ③ その結果、ProgressChanged イベントハンドラに登録されたメソッド(backgroundWorker1_ProgressChanged() メソッド)が、UI スレッド上で起動する

  • BackgroundWorker コンポーネントを用いた終了通知

    ① プールスレッド上で、backgroundWorker1_DoWork() メソッドが終了する

    ② BackgroundWorker は、内部で .BeginInvoke() を行い、メッセージキューにメッセージを投入

    ③ その結果、RunWorkerCompleted イベントハンドラに登録されたメソッド(backgroundWorker1_RunWorkerCompleted() メソッド)が、UI スレッド上で起動する

となります。つまり、UI スレッドとプールスレッドの橋渡しを、BackgroundWorker コンポーネントが行ってくれている、ということになるわけです。

改めて、どの処理がどのスレッド上で動作するのかをまとめてみると、

  • BackgroundWorker コンポーネント上の各メソッドをどのスレッド上で叩くか?

    ① BackgroundWorker.RunWorkerAsync() メソッドは、UI スレッド上から叩く。

    ② BackgroundWorker.ReportProgress() メソッドは、プールスレッドから叩く。

    ③ BackgroundWorker.CancelAsync() メソッド(後述)は、UI スレッド上から叩く。

  • BackgroundWorker コンポーネントに登録したイベントハンドラはどのスレッド上で動くか?

    ① DoWork イベントに登録したハンドラは、プールスレッド上で動く。(=UI 操作不可)

    ② ReportProgress イベントに登録したハンドラは、UI スレッド上で動く。(=UI 操作可)

    ③ RunWorkerCompleted イベントに登録したハンドラは、UI スレッド上で動く。(=UI 操作可)

ということになります。

では最後に、キャンセル処理についても実装してみましょう。

⑤ キャンセル機能の追加

Part 3. で述べたように、UI スレッドからタスクスレッドを強制的に停止させることはできないため、キャンセル処理は「UI スレッドからフラグを立てる」「タスクスレッドからフラグをチェックして自主的に止まる」ことになります。具体的には、以下の実装を行います。

  • backgroundWorker1 の WorkerSupportsCancellation プロパティを true に変更する。
  • btnCancel_Click() メソッドに、backgroundWorker1.CancelAsync() メソッドを呼び出す処理を記述する。
  • backgroundWorker1_DoWork イベントハンドラ内(タスクスレッドの長時間処理の中)に、キャンセルフラグを(なるべく頻繁に)チェックする処理を入れる。

追加されたコードは赤字部分です。ここまでの解説が理解できていれば、容易に理解できるのではないかと思います。

image

image

※ ちなみに実際に実行すると、キャンセルボタンを押した直後にプログレスバーが停止しませんが、これは Vista 以降でのコントロールのアニメーションの変更によるもの(アニメーションの遅延により発生する)です。XP などで実行すると、停止したタイミングでぴたっと止まります。

※ あと、書き忘れましたが、タスクスレッド上の例外処理についても書く必要がありません。タスクスレッド上で未処理例外が発生した場合には、RunWorkerCompleted イベントハンドラにて、e.Result で結果を取り出す際に例外がリスローされるため、特に例外処理のコードを追加しなくても、上のコードのままで集約例外ハンドラで例外を捕捉することができます。

このように、BackgroundWorker コンポーネントを利用すると、UI スレッド ⇔ タスクスレッドのスレッドスイッチに関連する処理を書く必要がなくなり、コードもかなりすっきりします。しかし、どの処理がどのスレッド上で動作しているのかを正確に理解しないと、非常に危険であるのも確かです。先に示した動作模式図を意識しながら、アプリケーションコードを記述するようにしてください。

[本エントリのまとめ]

では最後に、本エントリのまとめです。

  • ASP.NET 2.0 XML Web サービスのプロキシクラスは、フォーム画面上に貼り付けて使うことにより、Web サービス呼び出し処理を非同期処理化できる。
  • WCF サービスのプロキシクラスは、非同期処理メソッドを追加して使うことにより、呼び出し処理を非同期処理化できる。
  • 一般的なタスクについては、BackgroundWorker コンポーネントを使うことで非同期処理化ができる。

というわけで、4 回に渡ってマルチスレッドアプリケーションの開発手法について解説してきましたが、総じて言えば、

マルチスレッドアプリケーションを書くのはかなり難しい;。

ということになります。正しい知識を持って記述しないと、とにかくトラブルを引き起こしがちな技術になりますので、記述するのであれば十分な知識を持った上で、正しく記述するように心掛けていただければと思います。

※ なお、今回は Windows フォームに限定して解説を進めてきましたが、WPF や Silverlight にも同様な UI スレッド制限があります。WPF などを利用する場合には、こちらの MSDN マガジンのエントリなどを参考にしながら開発を進めていただければ幸いです。

Comments (3)

  1. aboutflat より:

    いつも拝見しております。

    >呼び出しが正常終了せず例外が発生した場合でも、webService1_GetMessageCompleted() イベントハンドラが UI スレッド上で呼び出される。そして、e.Result プロパティにアクセスした瞬間に、発生した例外がリスローされる。

    ちょうど、Silverlightアプリでの例外情報をどのように集約しようかと考えていたところなので、

    SilverlightでWebサービスを呼び出して例外が発生した時も同じことが出来れば、一つの解になるかなと思って試してみました。

    サービス側でどんな例外が出てもCommunicationExceptionになってしまっていて、

    サービス側で出ていた例外情報が消えてしまっていました。

    Silverlightでは、今回の例のようにWebサービスの例外を”正しく”集約して捕捉出来ないものなのですか?

    これがうまくいけば、クライアント側のApplication_UnhandledExceptionでクライアント側もサーバー側もまとめて例外が捕捉出来て、

    分離ストレージにでも書き出しておいで適宜、サーバーのログ格納先に反映したらいいかなとか思ってました。

    マルチスレッドアプリの開発の本筋とは違うかもしれませんが、気になったのでコメントさせていただきました。

  2. nakama より:

    aboutflat さん、こんにちは。コメントありがとうございます。

    > サービス側でどんな例外が出てもCommunicationExceptionになってしまっていて、

    > サービス側で出ていた例外情報が消えてしまっていました。

    > Silverlightでは、今回の例のようにWebサービスの例外を”正しく”集約して捕捉出来ないものなのですか?

    いえ、少なくとも aboutflat さんの書かれている内容についていうと、実はその挙動こそが正しい挙動なのです。:-)

    ポイントがいくつかありますので、ざっくり書くと以下の通りになります。

    ■ サーバ側で発生した未処理例外を、クライアントで同じ例外として捕捉することはできない。

    これはもともと XML Web サービスというものが異機種間接続を前提としていることなどが原因です。サーバ側で発生した未処理例外は、文字列情報に変換され、(SOAP fault 要素と呼ばれる XML タグを使って)SOAP メッセージの一部としてクライアントに返されます。クライアントでは、SOAP fault 要素の中から文字列を取り出して、以下のどちらかの例外として呼び出し元に返します。

    ・CommunicationException 例外(WCF プロキシの場合)

    ・SoapException 例外(ASP.NET XML Web サービスプロキシの場合)

    どちらの場合であっても、サーバ側と同じ例外情報を捕捉することはできません。

    ■ そもそもサーバ側で発生した未処理例外を、クライアントに対して公開するべきではない。

    上では、サーバ側で発生した未処理例外は文字列情報に変換され、SOAP fault 要素に格納されて返される、と書きましたが、本来はこの挙動自体、望ましくありません。というのも、サーバ側で発生した未処理例外に含まれるスタックトレース情報などにはアプリケーションの内部構造などの情報が含まれており、これをクライアント側に見せてしまうと、セキュリティ上のリスクになることがありうるからです。このため、

    ・サーバ側で発生した例外については、サーバ側の集約例外ハンドラで捕捉する。

    ・クライアントに対しては、例外が発生したということだけ通知し、詳細情報は返さない。

    という挙動をさせるのが適切です。

    ■ サーバ側で発生した未処理例外情報が、クライアントに公開されるか否かは、サーバ側のランタイムの作りや設定に依存する。

    さて、サーバ側で発生した例外情報はクライアントに対して公開すべきではない、と書きましたが、このことが言われるようになってきたのは(確か)2004 年のころでした。こうした歴史的経緯があるために、

    ・ASP.NET XML Web サービスの場合(*.asmx)、既定では例外の詳細情報を隠ぺいせず、SOAP Fault として返してしまう。(このため、ホントは例外の詳細情報を隠ぺいするための対処が必要。)

    ・WCF Web サービスの場合(*.svc)、既定では例外の詳細情報を隠ぺいし、SOAP Fault には「例外が発生した」という事実だけが記載される。

    という挙動になっています。(どちらが望ましい挙動かというと後者です。)

    もちろん、小規模イントラネットアプリケーションのように、そこまで気にしなくてもセキュリティ上の大きなリスクにはならない、ということもあるでしょう。こうした場合には、未処理例外情報をクライアントに返してしまってクライアント側ですぐに参照できるようにしてしまう、ということも可能です。WCF の場合には、serviceDebug ビヘイビアと呼ばれるものを使うことによりこれが実現できますが、あくまでこの機能はデバッグが目的の主体であり、aboutflat さんが書かれているような「クライアントでまとめて集約して記録する」ことが目的ではないことに注意してください。

    ちなみに一般論で言うと、クライアントでサーバ側の例外を集約記録してもあんまり嬉しくありません;。というのも、(エンドユーザが使っている)クライアント PC 上に例外ログが記録されてしまうと、サーバで発生した障害に対応するために、わざわざエンドユーザのところに連絡を取って、サーバの障害ログを送ってもらわなければなりません。サーバで発生した障害には、サーバ環境だけで対処できるようにした方が通常は便利なため、サーバ側の例外情報は、サーバ環境でロギングします。

  3. aboutflat より:

    回答ありがとうございました。

    なるほど、納得です。

    勉強になりました。

    そろそろ、またSilverlightのエントリーを期待しております。

Skip to main content