2017年4月22日土曜日

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

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

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


リソースの待ち合いによる「デットロック」
「Task.Wait()」は「Task」の処理終了を待つ間、
「メインスレッド」の動作を止めてしまいます。
その時に、
「ワーカースレッド」側から「Invoke」で
「メインスレッド」側の「Form1」内を触ろうとしても、
「メインスレッド」側は「ワーカースレッド」の処理終了まで
動作を止めている為、「メインスレッド」の動作が再開する迄
「ワーカースレッド」も処理を止めます。
すると、
「メインスレッド」も「ワーカースレッド」も、
永遠に処理が進まなくなります。これが、「デットロック」です。
「Invoke」と「Task.Wait()」による「デットロック」の例
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
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()」を残した方法だと、
「非同期」の処理を全て「タスク」の中で行う方法があります。
「Task.Wait()」での「非同期処理」化例
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
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」をしておいた方が良いです。
「async/await」で「Invoke」と「Task.Result」使用例
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
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」でキャッチできます。
「Task.Wait()」中の「例外」例
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
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()」を使います。
複数の「Task」をまとめる
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
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」で起きた「例外」全てをキャッチする事ができます。
「Task.Wait()」使用時の複数「例外処理」キャッチ例
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
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」で起きた「例外」のみをキャッチする事ができます。
「await」使用時の複数「例外処理」キャッチ例
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
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」内の処理が終了するのを待ち、処理が終了したら次の行へ処理を移します。
「タスク」内「Invoke」。「同期」
//「メインスレッド」側で処理を行ってもらうコードを記述。「同期」
 this.Invoke(new Action(() => {
     //「label1」の表示更新
     label1.Text = i.ToString();
 }));

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


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

「BeginInvoke」のタスク的使用例は以下の形になります。
「BeginInvoke」内で「Invoke」を使う事も可能。
「BeginInvoke」の使い方例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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()」と同じ)
「ワーカースレッド」に対して「引数」と「戻り値」を付ける場合の
記述もしておきます。
「EndInvoke」、「引数」、「戻り値」を使った処理例
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
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へ