次の方法で共有


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 = "https://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

  • Anonymous
    April 09, 2009
    いつも拝見しております。 >呼び出しが正常終了せず例外が発生した場合でも、webService1_GetMessageCompleted() イベントハンドラが UI スレッド上で呼び出される。そして、e.Result プロパティにアクセスした瞬間に、発生した例外がリスローされる。 ちょうど、Silverlightアプリでの例外情報をどのように集約しようかと考えていたところなので、 SilverlightでWebサービスを呼び出して例外が発生した時も同じことが出来れば、一つの解になるかなと思って試してみました。 サービス側でどんな例外が出てもCommunicationExceptionになってしまっていて、 サービス側で出ていた例外情報が消えてしまっていました。 Silverlightでは、今回の例のようにWebサービスの例外を”正しく”集約して捕捉出来ないものなのですか? これがうまくいけば、クライアント側のApplication_UnhandledExceptionでクライアント側もサーバー側もまとめて例外が捕捉出来て、 分離ストレージにでも書き出しておいで適宜、サーバーのログ格納先に反映したらいいかなとか思ってました。 マルチスレッドアプリの開発の本筋とは違うかもしれませんが、気になったのでコメントさせていただきました。

  • Anonymous
    April 13, 2009
    The comment has been removed

  • Anonymous
    April 15, 2009
    回答ありがとうございました。 なるほど、納得です。 勉強になりました。 そろそろ、またSilverlightのエントリーを期待しております。