2017年4月22日土曜日

C# 今時のマルチスレッド、非同期のやり方

最初に…


マルチスレッド(非同期処理)は
主にバックエンド処理を行う間、
フロントエンド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→45
と表示されます。
これは、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」型でないといけないので、
  //「パターン2」や「パターン3」を参照して下さい。

  //
  //オススメしないのですが、
  //この中に処理を投げっぱなしの
  //「async」「非同期メソッド」を作る事もできます。
  //後述している
  //      「UIイベント」に「async」を書かない方法
  //を参照して下さい。
}
何度も言いますが、「void」型の戻り値が許されるのは「UIイベント」のみです。
その為「await(非同期)」を使用したコードは必然的に、この後紹介する
  「パターン2」や「パターン3」
の記述方法となるはずです。
あまりオススメしないのですが後述する、
  「UIイベント」に「async」を書かない方法(パターン1補足)
のような「UIイベント」の書き方も可能です。
ただし本来(非同期メソッドを内包するメソッド)は、
「UIイベント」のメソッドであっても「async」修飾子を付けるべきです。


「Task」型の戻り値を使う場合(パターン2)
特に「戻り値」が必要ない場合は、「Task」型の戻り値を使用します。
  ・「return」は必要ない為、メソッド内に書きません。
  ・「非同期メソッド」自体の処理状態情報を知る事ができます。
  ・「非同期メソッド」名、語尾に「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」追加。
戻り値のある自作メソッドを「非同期メソッド」にする場合は、
必ず「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


独学なので、認識違いがあったらごめんなさい。
以上。



1 件のコメント:

  1. 「できマウス。」プロジェクトの町田と申します。丁寧な実例を交えての貴重な情報をありがとうございます。https://dekimouse.org/we/

    返信削除

↑Topへ