マルチスレッドで動くGUIアプリのプログラムを書いていると、以下のようなエラーに出くわすことがある人が多いのではないでしょうか。
有効ではないスレッド間の操作:コントロールが作成されたスレッド以外のスレッドからコントロール
textbox1
がアクセスされました。
処理が重い!並列処理にしよう!⇒なんかエラーでた!⇒ググる⇒invoke使えばいいんだ!
過去の自分を含め、多分みんなこの流れで解決まではたどり着く。でも、何がだめだったのか、どうして改善できるのか、invoke, delegateとは一体何なのかという事までちゃんと理解する人はあまりいないと思う。
本記事では、まずは方法を示して、スレッド間の処理の移譲の仕組みを深堀してみる。
エラーが出る例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace DelegateTest { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void SubThreadProcess() { textBox1.Text = "test"; return; } private void button1_Click(object sender, EventArgs e) { Task task = Task.Run(() => { SubThreadProcess(); }); return; } } } |
テキストボックスとボタンだけが配置されたUIを作り、ボタンを押したらテキストボックスに”test”と表示される処理を実装した。
テキストボックスの文字を変更する関数SubThreadProcess()
はTask.Run()
によって別スレッド(ワーカースレッド)で起動される。
このプログラムを起動し、ボタンを押すと下記の位置で例外が発生する。
解決方法
はい。invoke
とdelegate
を使ってください(笑)
以下のように修正する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace DelegateTest { public partial class Form1 : Form { public Form1() { InitializeComponent(); } delegate void DelegateProcess();//delegateを宣言 private void SubThreadProcess() { DelegateProcess process = new DelegateProcess(ChangeTextBox);//delegateにChangeTextBoxを登録 this.Invoke(process);//delegateを実行 return; } private void button1_Click(object sender, EventArgs e) { Task task = Task.Run(() => { SubThreadProcess(); }); return; } private void ChangeTextBox()//UIを変更する処理を関数にする { textBox1.Text = "test"; return; } } } |
なぜエラーになるのか
最初に示したエラーになるコードの内容を整理してみる。
ユーザーがボタンをクリックするとイベントハンドラbutton1_Click()
が実行される。
次に、Task.Run()
によってワーカースレッドが立ち上がり、SubThreadProcess()
が実行される。
この関数の中でテキストボックスの内容を変更しようとするところでエラーが発生する。
まず、C#のGUIアプリケーションプログラミングの大原則は
UIコントロールの変更はUIスレッドからのみ行える
ということ。これはこの言語、フレームワークの仕様である。
今回はテキストボックスの内容を”ワーカースレッドから”変更しようとしたことでエラーになってしまっている。
invokeでなぜ解決するのか
エラーが出る原因はただ一点、UIスレッド以外でUIの変更を行うからであった。であれば、UIスレッドで処理を行うようにワーカースレッドからUIスレッドへ処理を依頼すればよい。
UIスレッドへ処理を依頼するときに使うのがControl.Invokeメソッド。
https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.control.invoke?view=netcore-3.1
本プログラムではthis.Invoke(process);
という形で利用している。this
はオブジェクトForm1
を示す。Form1はテキストボックスやボタンなどUIコントロールを持つフォームのクラスである。そのクラスのメソッドの一つとしてinvoke()
が存在している。
invokeの引数にはUIスレッドに依頼したい処理を渡す。
しかし、this.Invoke(ChangeTextBox);
のように、関数をそのまま引数として渡すことはできない。
上記URLの公式ドキュメントを見ると、引数として渡すデータ型はdelegateということになっている。
じゃあ、そのdelegateって何なのか。
delegateとは
delegateは「関数を格納する箱」のようなデータ型と考えればいい。
正確には「関数の呼び出し先を格納する箱」。
関数をこの「箱」に入れて、箱をinvokeに引数として渡すことでUIスレッドで処理を実行できる。
delegateはまず宣言が必要。関数を格納する箱を定義する。
1 |
delegate void DelegateProcess();//delegateを宣言 |
ここまでだとデータ型を定義しただけの状態、つまり箱の形を定義しただけの状態である。
次に、定義した箱の実体(process)を宣言して、関数を格納(登録)する。
1 |
DelegateProcess process = new DelegateProcess(ChangeTextBox);//delegateにChangeTextBox関数を登録 |
たったこれだけ。あとは関数が登録された状態の箱process
をInvoke()
に渡してやればいい。
delegateについては別記事で詳しく解説してありますので、併せて参照して頂ければと思います↓