「C Sharpの基礎 - マルチスレッド」の版間の差分

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動
編集の要約なし
12行目: 12行目:
  public async Task<int> MethodAsync()
  public async Task<int> MethodAsync()
  {
  {
     var iRet = await Task.Run(() => {
     var iRet = await Task.Run(() =>
                                      await Task.Delay(5000));
                    {
                                      return 0;
                        await Task.Delay(5000);
                                    });
                        return 0;
                    });
     return iRet;
     return iRet;
  }
  }

2020年2月9日 (日) 18:26時点における版

概要

.NET Framework 4.5で導入されたC# 5.0(Visual Studio 2012) からは、マルチスレッドプログラミングをコンパイラレベルでサポートしている。
具体的には、asyncとawaitというキーワードが導入された。
これは、タスクベース非同期パターン(Task-based Asynchronous Pattern、TAP)を実装するものである。

このタスクベース非同期パターンでは、処理の開始はMethodNameAsyncという形式をとる。
そしてその戻り値を、Task、Task<T>、voidとし、Taskを待つにはawaitを付加する。
また、awaitを付加した関数には、asyncを付加する。

下記の例は、Task<int>型を返す非同期関数である。

 public async Task<int> MethodAsync()
 {
    var iRet = await Task.Run(() =>
                     {
                        await Task.Delay(5000);
                        return 0;
                     });
    return iRet;
 }



asyncとawaitの動作

Taskは、一連の処理をひとまとまりにした単位である。
Taskを呼び出した際はマルチスレッドで実行されるようになっており、連続で記述する場合は、記述した個数だけマルチスレッドで並列処理される。

 public async Task MethodAsync()
 {
    var task1 = Task.Run(() =>
                {
                   // 何か処理1
                });
 
    var task2 = Task.Run(() =>
                {
                   // 何か処理2
                });
 
    var task12  = Task.WhenAll(task1, task2);
 
    var taskAll = task12.ContinueWith(() =>
                  {
                     // 何か処理3
                  });
 
    return await taskAll.ConfigureAwait(false);
 }



Task.WhenAllとContinueWith

TaskはContinueWith関数を利用して、その後の処理を連続して実行できる。
2つ以上のスレッドを実行して全スレッドの処理が終了した後、続けてスレッドの処理を書く場合は、
ContinueWithまたはawaitを使用してスレッドの終了を待つ。

 // パターン1
 var task1 = Task.Run(() =>
             {
                // 何か処理
             });
 
 var task2 = Task.Run(() =>
             {
                // 何か処理
             });
 
 var task3 = Task.Run(() =>
             {
                // 何か処理
             });
 
 var taskAll = Task.WhenAll(task1, task2).ContinueWith(task3);  // awaitは不要


 // パターン2
 var task1 = Task.Run(() =>
             {
                // 何か処理
             });
 
 var task2 = Task.Run(() =>
             {
                // 何か処理
             });
 
 await task1.ConfigureAwait(false);
 await task2.ConfigureAwait(false);
 // 以下にtask3を記述


状況にもよるが、処理が膨大になると可読性が落ちるので、
先に関数で纏めて処理を記述して関数を呼ぶだけでタスク間の関係性が見通せるパターン1の方が良い。


ConfigureAwait(false)

TaskのWaitメソッドやResultは、デフォルトで元のスレッドへ戻ろうとする。
そのため、GUIアプリケーションでは、下記のように記述すると簡単にデッドロックが発生する。

 public void callMethod()
 {
    methodAsync().Wait();
 }
 
 public async Task methodAsync()
 {
    return await Task.Delay(1000);
 }


上記の場合、methodAsync().Wait()を呼んだ時点で親スレッドがスリープする。
次に、methodAsync()が終了後、親スレッドに戻ろうとするが、既に親スレッドがスリープしているため処理を続けることができない。
その結果、親スレッドは子スレッドを待ち続け、子スレッドは親スレッドに戻ろうとして、デッドロックが発生する。

対策として、以下のように記述するとデッドロックが回避できる。(戻るスレッドはどこでもよい場合)

 public void callMethod()
 {
    methodAsync().Wait();
 }
 
 public async Task waitAsync()
 {
    return await Task.Delay(1000).ConfigureAwait(false);
 }