Boost」など、さまざまなカンファレンスを企画・運営しています。BOOKS(コードジン・ブックス)は、CodeZineの連載からカットアップした、開発現場の課題解決に役立つ書籍シリーズです。Frameworkにおけるマルチスレッドプログラミングの解説です。四部作の「パート2」では、スレッドでフォームやコントロールを扱う方法や、「スレッドプール」「非同期デリゲート」などの事柄について学びます。Frameworkにおけるスレッドの実行や同期の方法などを学びました。本稿では、スレッドでフォームやコントロールを扱う方法や、「スレッドプール」「非同期デリゲート」などの事柄について解説します。対象読者この記事はパート1の続きですので、パート1からお読みください。1.1で動作確認をしています。別スレッドからフォーム、コントロールを扱うパート1で幾つかのサンプルを紹介してきましたが、すべてコンソールアプリケーションでした。実はこれには深い訳があります。Consoleクラスはスレッドセーフであり、マルチスレッド操作に対して安全が保障されています。ところが、Windowsアプリケーションのコントロール(フォームを含む)は違います。Controlクラスでスレッドセーフが保障されているのは、Invoke、BeginInvoke、EndInvoke、CreateGraphicsメソッドおよびInvokeRequiredプロパティのみです。Windowsフォームはシングルスレッドアパートメント(STA)モデルを使用しており、コントロールのメソッド(あるいはプロパティ)はそのコントロールを作成したスレッド(UIスレッド)からしか呼び出すことができません。スレッドセーフが保障されているメソッドを除き、コントロールのメソッドを別スレッドから直接呼び出してはいけません。これを解決するには、スレッドの境界を越えて実行を行う、マーシャリングが必要になります。FrameworkのControlクラスでは、マーシャリングを確実かつ効率よく行う方法として、Invoke、BeginInvoke、およびEndInvokeメソッドが用意されています。Invokeメソッドサンプル「form_01」は、Invokeメソッドを使用した簡単な例です。フォームに配置されたテキストボックスTextBox1にメインスレッドとは別のスレッドから文字列を追加しています。以下にコードの一部を抜粋します。TextBox1.Lines.Length;}Thread.CurrentThreadは、現在のスレッドのThreadオブジェクトを取得するプロパティです。Thread.Nameは、スレッドの名前を取得、設定するプロパティです。このコードでは、ボタンコントロールButton1をクリックすることにより、別スレッドからTextBox1に文字列を追加しています。TextBox1を作成したスレッド(ここではメインスレッド)とは別のスレッドからTextBox1のメソッドを直接呼び出すことはできませんので、TextBox1を操作するWriteLineメソッドを用意し、これをInvokeメソッドで呼び出します。このようにすることにより、WriteLineメソッドはメインスレッドで実行されるようになります。呼び出しもとのスレッドは[サブスレッド]現在のスレッドは[メインスレッド]と表示されます。Invokeメソッドで呼び出されたメソッドはメインスレッドで実行されていることが分かると思います。Invokeメソッドを使ってUIスレッド以外のスレッドからコントロールを操作する手順をまとめると、次のようになります。コントロールのメソッドやプロパティを呼び出すためのメソッドを作成する。そのメソッドに合ったデリゲートを宣言する。デリゲートオブジェクトを作成する。コントロール、あるいはコントロールのあるフォームのInvokeメソッドをデリゲートオブジェクトを指定して呼び出す。当たり前のことですが、Invokeで呼び出すメソッドはなるべく短時間で終了するようにしてください。あまりに長いと、別スレッドで実行する意味がなくなってしまいます。BeginInvoke・EndInvokeメソッドInvokeメソッドは同期的にデリゲートを実行するため、メソッドが終了するまでブロックします。これに対してBeginInvokeメソッドは非同期的にデリゲートを実行するため、ブロックしません。サンプル「form_02」がその例です。TextBox1.EndInvoke(ar);}EndInvokeメソッドは、BeginInvokeで呼び出したメソッドが終了するまでブロックします(よって上のコードは非同期としている意味がありません)。戻り値を取得する必要がなければ、EndInvokeを呼び出す必要はありません。InvokeやEndInvokeメソッドはスレッドをブロックするため、デッドロックの原因となる危険があります。戻り値を取得する必要がなければ、BeginInvokeメソッドのみを呼び出すのがより安全な方法です。InvokeRequiredプロパティあるコントロールのメソッドを呼び出すときに、Invokeメソッドを使用する必要があるか調べる方法として、Control.InvokeRequiredプロパティが用意されています。そのコントロールを作成したスレッドがInvokeRequiredを呼び出したスレッドと異なる場合はtrue、それ以外はfalseを返します。上記のWriteLinesメソッドを次のように書き換えることにより、riteLinesメソッドをメインスレッドから呼び出したときはWriteLineメソッドを直接呼び出すようにできます(サンプル「form_03」)。}}なおInvokeRequiredがfalseのときにInvokeでメソッドを呼び出しても特に問題はないようです。MethodInvoker・EventHandlerデリゲート今までの例ではInvokeで実行するデリゲートを独自に定義していましたが、すでに用意されているMethodInvokerまたはEventHandlerデリゲートを使うこともできます。これらのデリゲートを使用した方がより高速で実行されるということです。サンプル「form_04」では、EventHandlerデリゲートを使用しています。"]\r\n");}MethodInvokerとEventHandlerデリゲートを使用したときは、メソッドに引数を渡したり、戻り値を受け取ることができません。EventHandlerには2つの引数がありますが、これらに何を指定してもsenderはInvokeを呼び出したコントロール、eはEventArgs.Emptyとなります。待機ハンドル「待機ハンドル」と呼ばれるWaitHandleオブジェクトは、スレッドの同期に使われます。このWaitHandleは、後述するThreadPool.RegisterWaitForSingleObjectメソッドや非同期デリゲートで使用されますので、その前に説明しておきます。待機ハンドルの状態には「シグナル状態」と「非シグナル状態」の2つがあり、待機ハンドルをどのスレッドも所有していなければ「シグナル状態」、所有していれば「非シグナル状態」です。WaitHandle.WaitOneメソッドなどを使うことにより、待機ハンドルがシグナル状態になるまでスレッドをブロックすることができます。FrameworkではWaitHandleクラスから派生したクラスとして、ManualResetEvent、AutoResetEventおよびMutexクラスが用意されています。その内ここでは、ManualResetEventとAutoResetEventについて説明し、Mutexについてはパート3で説明します。ちなみにManualResetEventとAutoResetEventは、「同期イベント」または「イベントオブジェクト」と呼ばれています。ManualResetEventサンプル「waithandle_01」は、ManualResetEventを使った非常に簡単な例です。}}このアプリケーションでは、DoSomethingメソッドを別スレッドで実行しますが、[Enter]キーを押すまでこのスレッドは終了しません。このコードでは、まず非シグナル状態でManualResetEventオブジェクト「manualEvent」を作成します。メインスレッドではDoSomethingメソッドを実行する別スレッドを作成した後、[Enter]キーが押されるまで待機します。別スレッドではWaitHandle.WaitOneメソッドを呼び出し、manualEventがシグナル状態になるまで待機します。メインスレッドで[Enter]キーが押されるとWaitHandle.Setメソッドが呼び出され、manualEventがシグナル状態になります。すると、別スレッドのWaitOneメソッドによる待機が解除され、DoSomethingメソッドが終了します。AutoResetEvent次に、ManualResetEventにより、2つのスレッドを交互に実行する例を示します(サンプル「waithandle_02」)。}}このプログラムを実行すると、「1」と「2」が交互に出力されます。上記のコードではManualResetEvent.WaitOneメソッドの直後でResetメソッドを呼び出して非シグナル状態に戻していますが、ManualResetEventの代わりにAutoResetEventを使うことにより、この手間が省けます。AutoResetEventではシグナルを待機中のスレッドがすべて解放されると、自動的に非シグナル状態にリセットされるのです(待機中のスレッドがない場合は、無限にシグナル状態のままとなります)。上記のコードをAutoResetEventを使って書き換えると、次のようになります(サンプル「waithandle_03」)。}}ManualResetEvent、AutoResetEventおよびMutexクラスはWin32同期ハンドルをカプセル化したものですので、Monitorクラスと比較すると移植性に劣りますし、パフォーマンスも悪いようです。できることなら、Monitorクラスを使うべきです。Tipsやメールマガジン「.NETプログラミング研究」の発行人。モブプログラミングを取り入れて開発を効率化させるヒント9企業の7割超が「2025年の崖」を理解していないことが明らかに。モブプログラミングを取り入れて開発を効率化させるヒント9企業の7割超が「2025年の崖」を理解していないことが明らかに。モブプログラミングを取り入れて開発を効率化させるヒント9企業の7割超が「2025年の崖」を理解していないことが明らかに。5.0」がリリース新着記事一覧を見るイベント今後の予定を見るCodeZineは、株式会社翔泳社が運営するソフトウェア開発者向けのWebメディアです。「デベロッパーの成長と課題解決に貢献するメディア」をコンセプトに、現場で役立つ最新情報を日々お届けします。イラストの無断転載を禁じます。記載されているロゴ、システム名、製品名は各社及び商標権者の登録商標あるいは商標です。