2017年4月22日土曜日

C# マルチスレッド、非同期の補足

C# 今時のマルチスレッド、非同期のやり方
http://1studying.blogspot.com/2017/04/c.html
の補足情報です。
補足では、
「親スレッド」→「メインスレッド」
「子スレッド」→「ワーカースレッド」
として、正しい名称で説明しています。

「Task.Wait()」と「デットロック」と「Task.Result」


リソースの待ち合いによる「デットロック」
「Task.Wait()」は「Task」の処理終了を待つ間、
「メインスレッド」の動作を止めてしまいます。
その時に、
「ワーカースレッド」側から「Invoke」で
「メインスレッド」側の「Form1」内を触ろうとしても、
「メインスレッド」側は「ワーカースレッド」の処理終了まで
動作を止めている為、「メインスレッド」の動作が再開する迄
「ワーカースレッド」も処理を止めます。
すると、
「メインスレッド」も「ワーカースレッド」も、
永遠に処理が進まなくなります。これが、「デットロック」です。
        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);
                }
            });

            //ここは「非同期」なので「Form1」側の処理は止まりません。

            //↓ここで「同期処理」的になり「Form1」の処理が止まる。
            //「ワーカースレッド」の処理が終わるまで、
            //「メインスレッド」の処理を止めて待機
            task.Wait();
            //「デットロック」の為、ここまで処理が来ない。
            MessageBox.Show("処理終了");
        }
実行すると「Form1」がハングアップしたような状態になります。
「Task.Wait()」と「Invoke」は相性が悪い。
「async/await」を使った方が良いです。


「Task.Wait()」の「非同期処理」化と「デットロック」回避…
通常は「async/await」を使った方が良いのですが、
「Task.Wait()」を残した方法だと、
「非同期」の処理を全て「タスク」の中で行う方法があります。
        private void button1_Click(object sender, EventArgs e)
        {
            //「タスク」内の処理終了を待たずに、次の行へ処理が移ります。「非同期」
            var task1 = 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);
                }
            });

            //ここは「非同期」なので「Form1」側の処理は止まりません。

            //「タスク」内の処理終了を待たずに、次の行へ処理が移ります。「非同期」
            var task2 = Task.Run(() =>
            {
                //↓ここは「同期」処理となり「task2」内「ワーカースレッド」の処理は止まるが、
                //「タスク」内の為、「Form1」自体の処理は止まらない。
                task1.Wait();
                MessageBox.Show("処理終了");
            });

            //「task1」「task2」の処理終了を待たずに、
            //この行まで処理が下りてきます。
            //結果「非同期」となります。「Form1」側の処理は止まりません。
        }
「0→1→2→3→4→5」「処理終了」
と表示されます。


「async/await」で「Task.Result」使用時の注意
「Task.Result」の処理内には「Task.Wait()」が入っています。
「Task.Result」を使う時には必ず「Task」に対して、
「await」をしておいた方が良いです。
        private async void button1_Click(object sender, EventArgs e)
        {
            //「ワーカースレッド」で実行
            var task = Task<string>.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);
                }
                return "リザルト";
            });

            //「await」なので待っている間「Form1」の処理は止まらない。「非同期」
            await task;
            //戻り値取得
            var resultStr = task.Result;

            ////↓まとめた書き方でも良い。(通常こちらをつかいます!)
            ////「await」なので戻り値を待つ間「Form1」の処理は止まらない。「非同期」
            //var resultStr = await task;

            MessageBox.Show("処理終了:"+resultStr);
        }
「0→1→2→3→4→5」「処理終了:リザルト」
と表示されます。



「Task.Wait()」と「例外処理」


「Task.Wait()」と「例外」
「AggregateException」でキャッチできます。
        private void button1_Click(object sender, EventArgs e)
        {
            //「非同期」で実行
            var task = Task.Run(() =>
            {
                System.Threading.Thread.Sleep(1000); // 1秒待つ
                throw new Exception("例外タスク内"); //「例外」発生
            });

            //ここは「非同期」なので「Form1」側の処理は止まりません。

            try
            {
                //↓ここで「同期的」になり「Form1」側の処理が止まる。
                task.Wait();
            }        
            catch (AggregateException aex) // 「例外」キャッチ
            {
                foreach (var ex in aex.InnerExceptions)
                {
                    MessageBox.Show(ex.Message);
                }
            }
            MessageBox.Show("処理終了");
        }
「例外タスク内」→「処理終了」
と表示されます。



複数の「Task」をまとめる


複数の「Task」をまとめるには…
「Task.WhenAll()」を使います。
        private async void button1_Click(object sender, EventArgs e)
        {
            var tasks = new List<Task>(); // TaskをまとめるListを作成
            //「0」~「5」迄カウントアップ
            for(int i=0; i<6; i++)
            {
                var task = MyTaskRun(i); //複数の「Task」を作成
                tasks.Add(task); //TaskをListへ追加
            }         
            var allTask=Task.WhenAll(tasks);//すべてのタスクをまとめる。

            //「await」なので待っている間「Form1」の処理は止まらない。「非同期」
            await allTask;

            MessageBox.Show("処理終了");
        }

        private Task MyTaskRun(int sec)
        {
            return Task.Run(() =>
            {
                System.Threading.Thread.Sleep(sec * 1000); // sec秒待つ
                this.Invoke(new Action(() =>
                {
                    label1.Text = sec.ToString();
                }));
            });
        }
「0→1→2→3→4→5」「処理終了」
と表示されます。

「Task.WhenAll()」は引数を、
「Task.WhenAll(task1,task2,task3)」のように書く事も可能です。



 複数「Task」の複数「例外処理」


複数「Task」中で起きた複数「例外処理」のキャッチ
「Task.WhenAll()」でタスクをまとめてから、
「Task.Wait()」なら「AggregateException」
「await」なら「Exception」で
複数「Task」内で起きた「例外」をキャッチします。

「Task.Wait()」使用時の複数「例外処理」
それぞれの「Task」で起きた「例外」全てをキャッチする事ができます。
        private void button1_Click(object sender, EventArgs e)
        {
            //「非同期」で実行
            var task1 = Task.Run(() =>
            {
                System.Threading.Thread.Sleep(1000); // 1秒待つ
                throw new Exception("例外タスク1");
            });
            var task2 = Task.Run(() =>
            {
                System.Threading.Thread.Sleep(2000); // 2秒待つ
                throw new Exception("例外タスク2");
            });
            var task3 = Task.Run(() =>
            {
                System.Threading.Thread.Sleep(3000); // 3秒待つ
                throw new Exception("例外タスク3");
            });
            //「Task」をまとめる
            var task = Task.WhenAll(task1, task2, task3);

            //ここは「非同期」なので「Form1」側の処理は止まりません。

            try
            {
                //↓ここで「同期的」になり「Form1」側の処理が止まる。
                task.Wait();
            }
            catch (AggregateException aex)
            {
                foreach (var ex in aex.InnerExceptions)
                {
                    MessageBox.Show(ex.Message);
                }
            }
            MessageBox.Show("処理終了");
        }
例外が「AggregateException」でキャッチされ、
「例外タスク1」→「例外タスク2」→「例外タスク3」→「処理終了」
と表示されます。

「await」使用時の複数「例外処理」
最初に「Task」で起きた「例外」のみをキャッチする事ができます。
        private async void button1_Click(object sender, EventArgs e)
        {
            //「非同期」で実行
            var task1 = Task.Run(() =>
            {
                System.Threading.Thread.Sleep(1000); // 1秒待つ
                throw new Exception("例外タスク1");
            });
            var task2 = Task.Run(() =>
            {
                System.Threading.Thread.Sleep(2000); // 2秒待つ
                throw new Exception("例外タスク2");
            });
            var task3 = Task.Run(() =>
            {
                System.Threading.Thread.Sleep(3000); // 3秒待つ
                throw new Exception("例外タスク3");
            });

            try
            {
                //「Task」をまとめる
                //「await」なので待っている間「Form1」の処理は止まらない。「非同期」
                await Task.WhenAll(task1, task2, task3);
            }
            // 「await」では「例外」キャッチは最初の1つのみとなる
            catch (Exception ex) 
            {
                MessageBox.Show(ex.Message);
            }
            MessageBox.Show("処理終了");
        }
例外が「Exception」でキャッチされ、
「例外タスク1」→「処理終了」
と表示されます。


「BeginInvoke」と「EndInvoke」


「Invoke」と「BeginInvoke」の違い…
「Invoke」は「同期」(呼び出し元の処理は停止して待機)(SendMessage的)
「BeginInvoke」は「非同期」(呼び出し元の処理は継続)(PostMessage的)

「タスク」内「Invoke」。「同期」処理。
「Invoke」内の処理が終了するのを待ち、処理が終了したら次の行へ処理を移します。
 
                   //「メインスレッド」側で処理を行ってもらうコードを記述。「同期」
                    this.Invoke(new Action(() => {
                        //「label1」の表示更新
                        label1.Text = i.ToString();
                    }));

「タスク」内「BeginInvoke」。「非同期」処理。
「BeginInvoke」内の処理終了を待たずに、次の行へ処理が移ります。
 
                   //「メインスレッド」側で処理を行ってもらうコードを記述。「非同期」
                    this.BeginInvoke(new Action(() => {
                        //「label1」の表示更新
                        label1.Text = i.ToString();
                    }));


その他タスク的「BeginInvoke」「EndInvoke」…
タスク的な「BeginInvoke」と「EndInvoke」の使い方は、
現在「Task」クラスで代用する為、ほぼ使われていません。
概要程度でOK。

「BeginInvoke」のタスク的使用例は以下の形になります。
「BeginInvoke」内で「Invoke」を使う事も可能。
        private void button1_Click(object sender, EventArgs e)
        {
            //「非同期処理」本体
            var asyncWork = new Action(()=> {
                for (int i=0; i<6; i++) { 
                //「Invoke」も使える。
                this.Invoke(new Action(()=>{
                        label1.Text = i.ToString();
                    }));
                System.Threading.Thread.Sleep(1000);
                }
            });

            //「非同期処理」終了後に処理
            var endAsyncCallback = new Action<IAsyncResult>((ar) => {
                MessageBox.Show("非同期処理終了");
            });

            //「非同期処理」開始。コールバック付き。
            // 処理の終了を待たずに次の行へ処理が進みます。
            asyncWork.BeginInvoke(new AsyncCallback(endAsyncCallback), null);

            //「非同期」なので「Form1」側の処理は止まりません。
        }
「0→1→2→3→4→5」「非同期処理終了」
と表示されます。


「EndInvoke」を使うと「BeginInvoke」処理のコールバックを
「メインスレッド」側で待つ事ができます。
待っている間は「同期処理」となります。(「Task.Wait()」と同じ)
「ワーカースレッド」に対して「引数」と「戻り値」を付ける場合の
記述もしておきます。
        private void button1_Click(object sender, EventArgs e)
        {
            //「非同期処理」本体(戻り値付き、引数付き)
            var asyncWork = new Func<string,string> ((addText)=> {
                for (int i=0; i<6; i++) {
                    ////「Invoke」を使うと「デットロック」する為、使用しない。
                    //this.Invoke(new Action(()=>{
                    //        label1.Text = i.ToString()+addText;
                    //    }));
                    Console.WriteLine(i.ToString()+addText);
                    System.Threading.Thread.Sleep(1000);
                }
                return "戻り値";
            });

            //「非同期処理」開始。引数付き。コールバック無し(null)。
            // 処理の終了を待たずに次の行へ処理が進みます。
            var returnAr = asyncWork.BeginInvoke("秒経過", null, null);

            //ここは「非同期」なので「Form1」側の処理は止まりません。

            //↓ここで「同期処理」的になり「Form1」側の処理が止まる。
            //「ワーカースレッド」の処理が終わるまで、
            //「メインスレッド」の処理を止めて待機
            //(「Task.Wait()」と同じ。「Form1」が動かなくなる。)
            //戻り値の取得
            var resultStr=asyncWork.EndInvoke(returnAr);

            MessageBox.Show("処理終了:"+resultStr);
        }
「0→1→2→3→4→5」「処理終了:戻り値」
と表示されます。

「7行目」、
コメントにしている「Invoke」の「デットロック」を避けるには、
「27行目」以降の処理を「タスク」に内包して、
「Form1」の動作を止めないようにする。
等の方法があります。




ライブラリを作る場合、
自作のメソッドで「Task.Run()」は使用しない。
(「ConfigureAwait(false)」についての解説)
http://qiita.com/chocolamint/items/ed4999cccf011653cb78
http://qwerty2501.hatenablog.com/entry/2014/04/24/235849




0 件のコメント:

コメントを投稿

↑Topへ