最初に…
マルチスレッド(非同期処理)は
主にバックエンド処理を行う間、
フロントエンドUIの処理をフリーズさせない為に使います。
例えば、「Form」上の「ボタン」を押すと
なんらかの(バックエンド)処理が行われるソフトを作るとします。
その処理が行われている間、「Form」(フロントエンド)は
操作を受け付けなくなってしまいます。
(「Form」の移動等も出来ない。表示の更新も行われない。)
これを解決できるのがマルチスレッド(非同期処理)。
複数のスレッド処理を平行して行う事ができる為、
「Form」から他のスレッド処理を実行中に
「Form」自身の操作が可能。
「Form」自身の表示も正しく更新されます。
マルチスレッド(非同期処理)は
「Task」(lock)(Invoke)と
「async/await」(SemaphoreSlim)と
「例外処理」が使えればとりあえずOKみたいです。
使い方と周辺知識を自分なりにまとめてみました。
「Task.Wait()」については補足の方に書きました。
C# マルチスレッド、非同期の補足
http://1studying.blogspot.com/2017/04/c_22.html
に補足情報が書いてあります。
準備
「新しいプロジェクトを作成」
↓
「Windowsフォームアプリケーション」
↓
「Form1」へ
「ツールボックス」から
「Label」と「Button」を配置
↓
ボタンをダブルクリックして、
ボタンのクリックイベントを作成しておきます。
private void button1_Click(object sender, EventArgs e) { }
やりたい事
「button1」を押すと、
「label1」の表示が「0」〜「5」迄1秒毎にカウントアップ。
その間、「Form1」の動作が止まらないように処理させたいのです。
「Form1」の動作を止めない為には、
「label1」を「5」迄カウントアップ表示させるプログラムを
「子スレッド」で処理させる必要があります。
この時、「子スレッド」の処理が終わるまでの間、
「Form1」側の動作を止めて待つのが「同期処理」
「Form1」側の動作は止めずに待つのが「非同期処理」です。
「delegate(匿名メソッド、無名メソッド、関数)」と
「定義済みデリゲート」の「Action」「Func」型を使います。
これが分からない場合はリンク先を参照しておいて下さい。
http://1studying.blogspot.jp/2017/04/c-delegateactionfunc.html
ここでは説明上、「Task」内の処理を「子スレッド」とします。
本来は「親スレッド」を「メインスレッド」、
「子スレッド」を「ワーカースレッド」と言うのが正しいです。
(「Task」を使う側は気にする必要はない事なのですが、
「Task」は裏側で「Thread」や「ThreadPool」という処理を
状況により使い分けて作業を行っています。
その為、裏で1つのスレッドを複数「Task」で使い回す事があります。)
「Task」と「Invoke」
「Task」について…
「子スレッド」で処理を行う為に「Task」を使用します。
3つの使い方がありますが、
通常は「①」しか使わないので「①」だけ覚えておけばOKです。
① var task1 = Task.Run( デリゲート ); ② var task2 = Task.Factory.StartNew( デリゲート ); ③ var task3 = Task( デリゲート ); task3.Start();普段「①」の「デリゲート」の部分に「子スレッド」で処理させたい内容を書きます。
もし長時間の「子スレッド」実行が必要な場合は、
オプションの指定が可能な「②」を使う事もありますがほぼ使いません。
(「③」はStart()を行うまで「子スレッド」が実行されないタスクです。
「②Factory.StartNew」は「①Ran」と同じ物と思って差し支えありません。
①と②は「子スレッド」が即時に実行されます。
「.NET4.0」時代まで「②」が使われていたが、「.NET4.5」になり、
よりシンプルな「①」の方法が追加されました。)
「Invoke」について…
「インボーク」は「呼び出し」と言う意味です。
「Form1」から「Task」により、「子スレッド」が実行されたとします。
この「子スレッド」から「親スレッドのForm1」のデータを書き換える際、
「Invoke」が使われます。
(複数のスレッド間ではデータのやりとりが制限されている為、
この方法が使われます。「スレッドセーフな呼び出し」等と呼ばれます。)
「Invoke」は「子スレッド」内から
「親スレッドのForm1」に対して、処理を間借りさせてもらい、
「親スレッドのForm1」側の内部でコードを実行させる「メソッド」です。
(「親スレッドのForm1」で実行させたいコードは、
「子スレッド」内の「Invoke」の引数として、デリゲートを使い記述します。)
「Invoke」は「Form」配下の全てのコントロールで使用できます。
(「System.Windows.Formes名前空間」Controlクラスが
フォーム自身と配置される全てのコントロール(アイテム)に継承されている為です。
これにより、
「System.Windows.Formes.Control.Invoke(Delegate)」
の「メソッド」が継承され使用可能となる。)
処理の例…
これらを踏まえた処理が以下の形となります。
private void button1_Click(object sender, EventArgs e) { //「子スレッド」で実行 //コードはデリゲートで記述 var task = Task.Run(() => { //「0」~「5」迄カウントアップ for(int i=0; i<6; i++) { //「親スレッド」側で処理を行ってもらうコードを //デリゲートで記述 this.Invoke(new Action(()=> { //「label1」の表示更新 label1.Text = i.ToString(); })); //1秒待つ System.Threading.Thread.Sleep(1000); } }); //「子スレッド(カウントアップ)」の処理終了は待ちません //ここから先の処理はすぐに実行されます。 }「button1」をクリックすると「子スレッド」側から、
「0→1→2→3→4→5」と「親スレッドForm1」のラベル表示を更新します。
ラベル表示更新の最中に「Form1」側の動作は止まりません。
つまり「TaskとInvoke」を使用した「非同期処理」となっています。
「Invoke」の戻り値について…
「Invoke」の戻り値はobject型です。
もし上記処理で戻り値を得たい場合は「Invoke」部分の記述が以下の形になります。
var returnObj=this.Invoke(new Func<object>(() => { //「label1」の表示更新 label1.Text = i.ToString(); return (object)12341; }));「Invoke」は「子スレッド内で親スレッドの処理」を行います。
つまり「Invoke」で「親スレッド」の処理を行った際の戻り値を使用すれば
「子スレッド側」から「親スレッド(Form1)」の情報、
例えば「label1」のプロパティの値などが取得可能です。
「Task」内に処理を全て書く必要性について…
この後説明する「async/await」の所でこの事が
とても重要になります。
上記「TaskとInvokeを使用した処理例」のコードを再確認してみて下さい。
「Task」内の処理(ラベルのカウントアップ処理)が「子スレッド」側で実行中の間、
「親スレッドのForm1」側は「Task」が記述された次の行へ、
すぐに処理を移してしまいます。
「Task」内の処理終了を待ってはくれません。
「非同期処理」にしたいのは
「子スレッド」側で「Task」内の処理(カウントアップ処理)を行っている間、
「親スレッド」側の「Form1」の動作を止めたくないからでしたね。
その為には、
「子スレッド」で行う予定の全処理を「Task」内にまとめて記述する必要があるのです。
ただし、後述する「async/await」を使うと、
「Task」内に全ての処理をまとめる必要がなくなります。
「async/await」はとても便利な機能です。
「Task」と「invokerequired」
「InvokeRequired」について…
「InvokeRequired」プロパティは
自分は現在「子スレッド」側として実行中なのか?を教えてくれます。
例えば、
自分が「親スレッド(Form1と同スレッド)」であれば「Invoke」は必要無いので
直接「Label1」プロパティなどを操作すれば良いです。
しかし、
自分が「子スレッド」の場合「Invoke」を使用して(介して)
「親スレッド(Form1)」内の「Label1」プロパティなどを操作する必要があります。
つまり「InvokeRequired」プロパティは
「Form1」内コントロールの操作に「Invoke」が必要かどうかを判別する為のものです。
「Invoke」が必要な状態であればtrueを返します。
private void button1_Click(object sender, EventArgs e) { //「子スレッド」で実行 var task = Task.Run(() => { //「0」~「5」迄カウントアップ for(int i=0; i<6; i++) { //「label1」の表示更新 SetLabelStr(i.ToString()); //1秒待つ System.Threading.Thread.Sleep(1000); } }); //ここから先の処理は //「子スレッド」の処理終了を待たずに即実行されます //「親スレッド側」であるここからでも「SetLabelStrメソッド」を使う事ができます。 } //「label1」の表示更新(親スレッド側と子スレッド側の両方の呼び出しに対応) private void SetLabelStr(string text) { //「子スレッド」で実行中かどうか //(「Invoke」が必要かどうか) if (this.label1.InvokeRequired) { //「親スレッド」側で処理を行ってもらうコードを //デリゲートで記述 this.Invoke(new Action(() => { label1.Text = text; })); return; } else { this.label1.Text = text; } }「SetLabelStr」メソッド内で「InvokeRequired」を使用して、
「lavel1」への文字列表示更新処理を振り分ける形にする事で、
「SetLabelStr」メソッドがクラス内のどこからでも使える形となります。
自分が「Task」内かを意識せずに「SetLabelStr」メソッドが使えることで
扱いやすいメソッドと言えます。
「Task」と「lock」
「lock」について…
上述の「InvokeRequiredを使用した処理例」のコードを実行して、
「button1」をゆっくり2回クリックすると、
「0→1→2→3→4→5」「0→1→2→3→4→5」
と「直列的」に処理されて表示されるのではなく、
「0→1→0→2→1→3→2→4→3→5→4→5」
と表示されます。
これは、2回のクリックにより実行される2つの「子スレッド」が、
「並列的」に処理される為です。
1回目のクリックで「子スレッド」が実行、動作中の間は、
2回目以降のクリックによる「子スレッド」の動作を順番待ちさせておくのが
「lock」です。
(動作中の「子スレッド」処理が終了すると、
順番待ちしている他の「子スレッド」が順番に動作中になります。)
これにより「直列的」に処理されているように動作します。
//「lock」用オブジェクト object lockObj = new object(); private void button1_Click(object sender, EventArgs e) { //「子スレッド」で実行 var task = Task.Run(() => { //「lock」を使い「子スレッド」処理を順番待ちさせる。 lock (lockObj) { //「0」~「5」迄カウントアップ for (int i=0; i<6; i++) { //「label1」の表示更新 SetLabelStr(i.ToString()); //1秒待つ System.Threading.Thread.Sleep(1000); } } }); } //「label1」の表示更新 private void SetLabelStr(string text) { //「子スレッド」で実行中かどうか //(「Invoke」が必要かどうか) if (this.label1.InvokeRequired) { //「親スレッド」側で処理を行ってもらうコードを //デリゲートで記述 this.Invoke(new Action(() => { label1.Text = text; })); return; } else { this.label1.Text = text; } }これで「button1」をゆっくり2回クリックした場合、
「0→1→2→3→4→5」「0→1→2→3→4→5」
という「直列的」表示になります。
「lock」を使ってはいけない場面…
「lock」内に「await」は使えません。
「await」内に「lock」も使えません。
「await」中のロックは「semaphoreSlim」を使います。
「await」と「semaphoreSlim」については後述します。
「async/await」
「async/await」について…前説
「async/await」は「Task」をとても簡単に書く方法です。
普通だと「親スレッドのForm1」のコードの中で
「子スレッド」が「Task」内の処理を行っている間、
「親スレッドのForm1」側は「Task」が記述された次の行へ、
すぐに処理を移してしまいます。
「Task」内の処理終了を待ってはくれません。
「await」を使うと、
「親スレッドのForm1」の動作自体は止めないまま、
「Task」内の処理が終了するまで
「await」が記述された次の行への処理を待機させる事ができます。
「await」は裏側で以下のような動作をしています。
非同期で「Task」を使用して「子スレッド」を呼ぶ。 この時の「Task」の行位置を記録し、そのまま「親スレッドのForm1」自体の通常動作処理へ移行。 「Task」内の処理が終了したら「子スレッド」から、記録した行位置まで制御を戻し処理を再開。「親スレッド」の動作を止めないで、
「子スレッド」からのコールバックを待つ。
みたいな動作になります。
「await」を使う事により、
前述の「InvokeRequiredを使用した処理例」コード内の
「SetLabelStr」メソッドのような書き方が必要なくなります。
(前述ではコントロールを「親スレッド」と「子スレッド」から
同じようにアクセスする為には
「SetLabelStr」メソッドのような書き方が必要だった)
コードの記述内で、
「async」修飾子が付いたメソッドを「非同期メソッド」と呼びます。
これは、
「async」付きメソッド内配下には
非同期処理を使用した「await」を
1つ以上使用していますよ。
という「印」となるものです。
「async/await」について…実践
「非同期メソッド」の処理例は以下の形になります。
//「async」修飾子が付くと処理配下で「await」を1回以上使用された印。 //その為このメソッドは「非同期メソッド」となる。 private async void button1_Click(object sender, EventArgs e) { //「0」~「5」迄カウントアップ for (int i = 0; i < 6; i++) { //「label1」の表示更新 label1.Text = i.ToString(); //「子スレッド」処理終了まで「await」で待機。「非同期」 await Task.Run(() => { //1秒待つ(実際では重い処理だったりする部分) System.Threading.Thread.Sleep(1000); }); } MessageBox.Show("処理終了"); }「button1」をクリックすると、
「0→1→2→3→4→5」「処理終了」
の順で表示されます。
「await」を使えば
「Form1」の動作を止めてしまいそうな処理が重そうな部分だけを
部分的に「Task(子スレッド)」として実行させる事が出来ます。
「await」の「Task(子スレッド)」で処理が行われているあいだ、
「Form1」の動作は止まらない状態で、
コード次行処理への進行のみが止まった状態となります。
「Task(子スレッド)」の処理が終われば、
「await」の次の行がら処理が再開されます。
スマートですね。
「await」は「子スレッド」の処理終了を待機させる物なので
「子スレッド」の実行開始位置と、「子スレッド」の終了待機の位置を
別の位置に書く事もできます。
以下のような書き方になります。
//「async」修飾子が付き「非同期メソッド」となる。 private async void button1_Click(object sender, EventArgs e) { //「0」~「5」迄カウントアップ for (int i = 0; i < 6; i++) { //「label1」の表示更新 label1.Text = i.ToString(); //「子スレッド」を実行。 //この書き方をすると「子スレッド」の処理終了を待たずに、 //次の行へ処理が移ります。(awaitが無い為) //戻り値は「Task」型になります。 var task1 = Task.Run(() => { //1秒待つ System.Threading.Thread.Sleep(1000); }); //「親スレッド」側の処理をここに書く事もできます。 //「子スレッド」を無視して「並列的」に処理されます。 //「await」で待機。「非同期」 //ここで「子スレッド」処理終了まで待機します。 await task1; //「子スレッド」処理が終了た後に、 //この行から処理が再開します。 } }
「Invoke」と「lock」について…
ここでは記述していませんが、
「await」の「Task」内に「Invoke」を使用しても問題ありません。
あと注意として、
ロックは「lock」でなく、「SemaphoreSlim」を使用します。
詳しくは後述します。
「async/await」の戻り値
「async」を付けた「非同期メソッド」は
戻り値の型が
「void」か「Task」か「Task<T>」か「ValueTask<T>」
のいずれかである必要があります。
(「ValueTask<T>」型はまだあまり使われていない為、今回説明を省きます。)
戻り値が「void」以外の時は「非同期メソッド」名の最後に、
「Async」か「TaskAsync」という語尾を付ける決まりになっています。
強制ではありませんが付けた方が無難です。
ただし、
「UIイベント」( button1_Click等)のメソッド名称は変更不要です。
「async」の戻り値3パターン
メソッド内で「await」や「async」が使われているメソッドの
戻り値パターンを紹介します。
戻り値のパターンにより
メソッド内で「return」を書く必要の有無
タスク内の処理状況の把握が可能、不可能
メソッド名の名称変更が必要、不必要
と違いが出ます。
「UIイベント」のみ「void」の戻り値がゆるされますので、
それについても説明します。
「void」型の戻り値を使う場合(パターン1)
「void」型の戻り値は
「UIイベントメソッド(button1_Clickなど)」でしか使ってはいけません。
・「return」は必要ない為、メソッド内に書きません。
・「非同期メソッド」自体の処理状態情報を把握できません。
・「非同期メソッド」名、変更なし。
「イベントメソッド」の戻り値は基本「void」型です。・「非同期メソッド」自体の処理状態情報を把握できません。
・「非同期メソッド」名、変更なし。
その「イベントメソッド(コントロール系のイベント)」を
「async」を付けた「非同期メソッド」に変更する時に限り
「void」型の戻り値が許可されます。
「void」型の戻り値では、「非同期メソッド」の処理状態情報を
「非同期メソッド」を実行した側が知ることができません。
その為、
「Task」の完了も報告されず、例外も知る事ができません。
つまり、「子スレッド」へ処理を投げたら投げっぱなしの形になります。
「イベントメソッド」以外を「非同期メソッド」に変更する場合は
この後紹介する「Task」型か「Task<T>」型の戻り値を使用して下さい。
//「void」型の戻り値が許されるのは、 //特殊な場合のみです。 //例えば、「ボタンクリックイベント」などは、 //「UIイベントメソッド」なので「void」型の戻り値が許されます。 private async void button1_Click(object sender, EventArgs e) { // //「button1_Click」イベントに「async」を記述した為、 //本来ならこのコード内で「await」を一度以上は使用しなければなりません。 //が、 //「await」の戻り値は「void」型はダメ。 //「TaskやTask何度も言いますが、「void」型の戻り値が許されるのは「UIイベント」のみです。」型でないといけないので、 //「パターン2」や「パターン3」を参照して下さい。 // //オススメしないのですが、 //この中に処理を投げっぱなしの //「async」「非同期メソッド」を作る事もできます。 //後述している // 「UIイベント」に「async」を書かない方法 //を参照して下さい。 }
その為「await(非同期)」を使用したコードは必然的に、この後紹介する
「パターン2」や「パターン3」
の記述方法となるはずです。
あまりオススメしないのですが後述する、
「UIイベント」に「async」を書かない方法(パターン1補足)
のような「UIイベント」の書き方も可能です。
ただし本来(非同期メソッドを内包するメソッド)は、
「UIイベント」のメソッドであっても「async」修飾子を付けるべきです。
「Task」型の戻り値を使う場合(パターン2)
特に「戻り値」が必要ない場合は、「Task」型の戻り値を使用します。
・「return」は必要ない為、メソッド内に書きません。
・「非同期メソッド」自体の処理状態情報を知る事ができます。
・「非同期メソッド」名、語尾に「Async」追加。
戻り値のない自作のメソッドを「非同期メソッド」にする場合は、・「非同期メソッド」自体の処理状態情報を知る事ができます。
・「非同期メソッド」名、語尾に「Async」追加。
必ず「Task」型の戻り値を使います。
//「async」修飾子が付き「非同期メソッド」となる。 private async void button1_Click(object sender, EventArgs e) { //「0」~「5」迄カウントアップ for (int i = 0; i < 6; i++) { //「label1」の表示更新 label1.Text = i.ToString(); //「子スレッド」を実行し、処理終了を待たずに次の行へ処理が移ります。 //戻り値は「Task」型です。 var task1 = Wait1secAsync(); //〜〜〜 //「子スレッド」処理終了まで「await」で待機。「非同期」 await task1; //「子スレッド」の処理が終了したらこの行から処理再開 } } //「async」修飾子が付き「非同期メソッド」となる。 //戻り値「Task」型 private async Task Wait1secAsync() { //1秒待つ await Task.Run(() => { System.Threading.Thread.Sleep(1000); }); }
「Task<T>」型の戻り値を使う場合(パターン3)
「戻り値」が欲しい場合「Task<T>」型の戻り値を使用します。
・「return」に戻り値を書きます。
「return」された型は「Task<T>」型で戻り値で受け取ります。
(「int」型を「return」する場合、「Task<int>」型で受け取る。)
・「非同期メソッド」自体の処理状態情報を知る事ができます。
・「非同期メソッド」名、語尾に「Async」追加。
戻り値のある自作メソッドを「非同期メソッド」にする場合は、「return」された型は「Task<T>」型で戻り値で受け取ります。
(「int」型を「return」する場合、「Task<int>」型で受け取る。)
・「非同期メソッド」自体の処理状態情報を知る事ができます。
・「非同期メソッド」名、語尾に「Async」追加。
必ず「Task<T>」型の戻り値にします。
「await」で「子スレッド」の処理が終了するのを待機してから
「Task<T>.Result」を使い、受け取った戻り値の取り出しを行います。
(「Task<T>.Result」には「Task.Wait()」処理が内包されています。
使用前には「await」を使って「子スレッド」の処理終了を待つ事を
忘れないように注意して下さい。)
この例では、文字列処理をあえて「子スレッド」側で行います。
//「async」修飾子が付き「非同期メソッド」となる。 private async void button1_Click(object sender, EventArgs e) { var addStr = ""; //「0」~「5」迄カウントアップ for (int i = 0; i < 6; i++) { //「label1」の表示更新 label1.Text = i.ToString()+addStr; //「子スレッド」を実行し、処理終了を待たずに //次の行へ処理が移ります。 //戻り値は「Task<T>」(ここでは「Task<string>」)型です。 var task1 = WaitSecAsync(1000); //~~~ //「子スレッド」処理終了まで「await」で待機。「非同期」 await task1; //「子スレッド」の戻り値から結果を得る addStr = task1.Result; } } //「async」修飾子が付き「非同期メソッド」となる。 //戻り値「Task<T>」(ここでは「Task<string>」)型 private async Task<string> WaitSecAsync(int num) { //1000分のnum秒待つ var sec = await Task.Run(() => { System.Threading.Thread.Sleep( num ); return num/1000; // 秒を返す。secへ格納。 }); return "×"+sec.ToString()+"秒経過"; // "×?秒経過"文字列を返す }ボタンクリックで、
「0→1×1秒経過→2×1秒経過→3×1秒経過→4×1秒経過→5×1秒経過」
と表示されます。
「UIイベント」に「async」を書かない方法(パターン1補足)
「void」型の戻り値は「UIイベント」のみに許されます。
「ActionやFunc」内でラムダ式に「async」を使う事により
「button1_Clickイベント」メソッドを「非同期メソッド」にしないで、
「button1_Clickイベント」メソッドはそのままに、
「Task」型の戻り値でラムダ式の「非同期メソッド」を内包させる事ができます。
以下の形になります。
private void button1_Click(object sender, EventArgs e) { //ラムダ式で「async」「非同期メソッド」作る。 Func<Task> func1 = async () => { //「0」~「5」迄カウントアップ for (int i = 0; i < 6; i++) { //「label1」の表示更新 label1.Text = i.ToString(); //「子スレッド」処理終了まで「await」で待機。「非同期」 await Task.Run(() => { //1秒待つ System.Threading.Thread.Sleep(1000); }); } }; //作った「非同期メソッド」の実行 //戻り値「Task」型 var task0=func1(); }その場限りの「非同期」のテストを行いたい時などに、
とりあえずこの書き方で動作確認してみる事がよくあります。
ただし本来は、
何か特別な意図がない場合は「UIイベント」のメソッドにも
「async」修飾子を付けるべきで、
必然的に
「パターン2」や「パターン3」
の記述方法となるハズです。
「async/await」中のロック、
「Task」と「SemaphoreSlim」
「lock」内に「await」は使えません。
「await」内に「lock」も使えません。
(複数の「await」の「子スレッド」を順に実行した時に、
同じ「スレッド」で動く事が保証されていない為です)
「await」中のロックは「semaphoreSlim」を使います。
「セマフォスリム」は、
事前に「セマフォロック」用のオブジェクトを用意しておいて、
「try{ 〜 }finally{ 〜 }」にからめて使用します。
//「セマフォロック」用「SemaphoreSlim」オブジェクト //(コンストラクタ時の引数は並列を許可する「子スレッド」の数) private System.Threading.SemaphoreSlim _semaphoreLock = new System.Threading.SemaphoreSlim(1); private void button1_Click(object sender, EventArgs e) { //ラムダ式で「async」「非同期メソッド」作る。 Func<Task> func1 = async () => { await _semaphoreLock.WaitAsync(); // 「セマフォロック」 try { //「0」~「5」迄カウントアップ for (int i = 0; i < 6; i++) { //「label1」の表示更新 label1.Text = i.ToString(); //「子スレッド」処理終了まで「await」で待機。「非同期」 await Task.Run(() => { //1秒待つ System.Threading.Thread.Sleep(1000); }); } } finally { _semaphoreLock.Release(); // 「セマフォロック」解除 } }; //作った「非同期メソッド」の実行 //戻り値「Task」型 var task0 = func1(); }これで「button1」を素早く2回クリックした場合、
「0→1→2→3→4→5」「0→1→2→3→4→5」
という「直列的」表示になります。
「他のスレッド」内で「例外」発生時の注意
「VisualStudio」の「デバッグモード」で、
「子スレッド」や「子スレッド」内「Invoke」等、
「他のスレッド」内で「例外」が起きた際の挙動に注意して下さい。
例えば、
private async void button1_Click(object sender, EventArgs e) { try { //「await」で実行。「非同期」 await Task.Run(() => { MessageBox.Show("タスク処理中"); throw new Exception("例外タスク内"); //「例外」発生 }); MessageBox.Show("処理"); } catch (Exception ex) // 「例外」キャッチ { MessageBox.Show(ex.Message); } MessageBox.Show("処理終了"); }は、
『「タスク処理中」→「例外タスク内」→「処理終了」と表示されます。』
と普段は書きますが、
実際の「VisualStudio」上では以下のような表示となります。
↓「タスク処理中」表示
↓ここで「スレッド」を跨ぐ為、エラー表示。
「ユーザーが処理していない例外」のメッセージ。
(「F5」を押し、「続行」させるだけで大丈夫です。)
↓「例外タスク内」表示
↓「処理終了」表示
この様に、「スレッド」を跨ぐ時に、
「VisualStudio」側のメッセージが介在する事を認識しておいて下さい。
最初から「VisualStudio」上で、
「CTRL+F5」の「デバッグ無しで開始」で実行を開始する事で、
「VisualStudio」側のメッセージの介在を防ぐ方法も有ります。
「async/await」と「例外処理」
「Task」内の例外を拾うには…
「await」「Task.wait()」「Task.result」を
「try〜catch」で囲む事で「Task」内の「例外」を掴めます。
(「await」以外の「例外処理」については補足で説明します。)
ここでは「await」での「例外処理」について説明します。
「Task.Run()」その物を「try〜catch」で囲んでも「例外」を
拾えない事に注意して下さい。
「await」中の「例外」
「Exception」でキャッチできます。
private async void button1_Click(object sender, EventArgs e) { //「子スレッド」で実行 var task = Task.Run(() => { this.Invoke(new Action(() => { label1.Text = "abcde"; })); System.Threading.Thread.Sleep(1000); // 1秒待つ throw new Exception("例外タスク内"); //「例外」発生 }); try { //「await」なので待っている間「Form1」の処理は止まらない。「非同期」 await task; } catch (Exception ex) // 「例外」キャッチ { MessageBox.Show(ex.Message); } MessageBox.Show("処理終了"); }「abcde」→「例外タスク内」→「処理終了」
と表示されます。
「await」中「Invoke」内の「例外」
「Exception」でキャッチできます。
「Invoke」内の「例外」を「await」内でキャッチ。
「await」内の「例外」を「親スレッド」内でキャッチしています。
「await」内で「例外」を再スローする際、
「throw;」は「throw ex;」としていない所にも注意して下さい。
(Javaの場合の「throw ex;」とC#の「throw;」が同義となります。
「throw;」→throwされる例外「StackTrace」を保持。
「throw ex;」→例外「StackTrace」をその時点から更新。)
private async void button1_Click(object sender, EventArgs e) { //「子スレッド」で実行 var task = Task.Run(() => { try { this.Invoke(new Action(() => { throw new Exception("例外Invoke内"); //「例外」発生 label1.Text = "abcde"; })); System.Threading.Thread.Sleep(1000); // 1秒待つ } catch (Exception ex) // 「例外」キャッチ { MessageBox.Show(ex.Message); throw; // 再スロー } }); try { //「await」なので待っている間「Form1」の処理は止まらない。「非同期」 await task; } catch (Exception ex) // 「例外」キャッチ { MessageBox.Show(ex.Message); } MessageBox.Show("処理終了"); }「例外Invoke内」→「例外Invoke内」→「処理終了」
と表示されます。
「上位スレッド」のみで「例外」をキャッチしたい場合は、
「Task」内の「Try{ }catch(Exception ex){ 〜 }」そのものを削除してもかまいません。
「Task」内の「例外処理」を残すのであれば、
try { 〜 } catch { throw; // 再スロー }の様に「(Exception ex)」を省略して再スローさせても、
「上位スレッド」で「例外」をキャッチできます。
「Task.Wait()」と「例外」
「AggregateException」でキャッチできます。
「Task.Wait()」についてはここでは説明しません。
補足の「Task.Wait()」と「例外処理」の方で説明しています。
他
C# マルチスレッド、非同期の補足
http://1studying.blogspot.com/2017/04/c_22.html
に補足情報を書いておきました。
「Task.Wait()」は「Task」の処理終了を待つ間、
「親スレッド」の動作を止めてしまいます。
「Invoke」や「await」との組み合わせでデットロックが紛れやすいです。
補足の方で説明しています。
「Task.Wait()」の「例外」についても
補足の方で説明しています。
以下サイトを参考にしました。
http://gomocool.net/gomokulog/?p=762
http://kimux.net/?p=902
http://qiita.com/acple@github/items/8f63aacb13de9954c5da
http://blog.xin9le.net/entry/2012/07/30/123150
https://ufcpp.wordpress.com/2012/11/12/asyncawait%E3%81%A8%E5%90%8C%E6%99%82%E5%AE%9F%E8%A1%8C%E5%88%B6%E5%BE%A1/
http://blog.shtair.net/2014/07/29/%E9%9D%9E%E5%90%8C%E6%9C%9F%E3%81%AE%E5%90%8C%E6%9C%9F/
非同期のパターン
http://qiita.com/0xfffffff7/items/f7ed068350ed420e5219
独学なので、認識違いがあったらごめんなさい。
以上。
「できマウス。」プロジェクトの町田と申します。丁寧な実例を交えての貴重な情報をありがとうございます。https://dekimouse.org/we/
返信削除