はじめに
Windows Forms アプリケーションなどでマルチスレッドアプリケーションを作成するときの注意として、フォームやコントロールへは、それらを作成したスレッドからアクセスする必要があります。
お題のアプリケーション
こんな感じのフォームで、1秒ごとに現在時刻を更新表示するアプリケーションを作成してみます。
フォームを作成したスレッドからタスクを生成(=別のスレッド)して更新表示をさせるしくみの、こんな感じのコードを実行させると、タスク内でラベルコントロール (label1) のテキストを更新しようとしたとき「InvalidOperationException : 有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'label1' がアクセスされました。」例外が発生してしまいます。
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp2
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
Task.Run(() =>
{
for(; ; )
{
label1.Text = DateTime.Now.ToString("HH:mm:ss"); // <- 例外発生
System.Threading.Thread.Sleep(1000);
}
});
}
}
}
専ら作業用に作成したスレッド(ワーカースレッド)から、フォームやコントロールなどが属するスレッド(UIスレッド)に処理をさせるには、Control.Invoke メソッドを使用する必要があり、ラベルコントロールのTextプロパティへ書き込む処理のコードを次のように変更するとうまく動きます。
Task.Run(() =>
{
for (; ; )
{
Invoke((MethodInvoker)delegate
{
label1.Text = DateTime.Now.ToString("HH:mm:ss");
});
System.Threading.Thread.Sleep(1000);
}
});
果たして、この Invoke メソッドは、どんなことをしているのでしょうか?
Invokeメソッドの処理
.NET Framework (4.7.2) のソースコードを読んでみました。かなりおおまかに書くと次のことをしています。
- Win32API の GetWindowsThreadProcessId 関数を使用して、アクセスしようとしているスレッドと、対象のコントロール(ハンドルが作成されていなければその親コントロール)の属するスレッドが一致しているか調べる。
- 処理するメソッド情報を溜めるキューにメソッド情報(メソッド、引数、実行コンテキスト、戻り値や発生した例外の受け取り場所、System.IAsyncResult をインプリメント)を追加し、1.でスレッドが異なればウインドウメッセージ(スレッドコールバック用:4.7.2では"WindowsForms12_ThreadCallbackMessage")を POST する。1.で同じスレッドであれば、自分自身でキューの中身を取り出してメソッドを呼び出す(キューが空になるまで繰り返す)。
- たったいまキューに追加した対象のメソッドが終了するまで待機(IAsyncResult.AsyncWaitHandle)し、その戻り値を返す。
POSTされた先のUIスレッドのメッセージポンプ(WndProc)では、スレッドコールバック用のウインドウメッセージを受け取ったら、キューを取り出してメソッドを(キューが空になるまで繰り返し)呼び出します。
ちなみに Control.InvokeRequired プロパティは、上記のうち 1. の処理を行い、同じスレッドであれば false、異なれば true を返します。
おわりに
ちょっと興味がありましたので、調べてみました。.NET Framework のソースコードを簡単に調べられる、Visual Studio の拡張機能「Ref12」にはいつもお世話になっています。