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

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動
(Wiki がページ「非同期プログラム(C Sharp)」を「マルチスレッドの使用方法(C Sharp)」に、リダイレクトを残さずに移動しました)
編集の要約なし
1行目: 1行目:
== 概要 ==
== 概要 ==
.NET Framework 4.5で導入されたC# 5.0(Visual Studio 2012) からは、マルチスレッドプログラミングをコンパイラレベルでサポートしている。<br>
具体的には、asyncとawaitというキーワードが導入された。<br>
これは、タスクベース非同期パターン(Task-based Asynchronous Pattern、TAP)を実装するものである。<br>
<br>
このタスクベース非同期パターンでは、処理の開始はMethodNameAsyncという形式をとる。<br>
そしてその戻り値を、Task、Task<T>、voidとし、Taskを待つにはawaitを付加する。<br>
また、awaitを付加した関数には、asyncを付加する。<br>
<br>
下記の例は、Task<int>型を返す非同期関数である。<br>
<source lang="c#">
public async Task<int> MethodAsync()
{
    var iRet = await Task.Run(() => {
                                      await Task.Delay(5000));
                                      return 0;
                                    });
    return iRet;
}
</source>
<br><br>
<br><br>


5行目: 24行目:
Taskは、一連の処理をひとまとまりにした単位である。<br>
Taskは、一連の処理をひとまとまりにした単位である。<br>
Taskを呼び出した際はマルチスレッドで実行されるようになっており、連続で記述する場合は、記述した個数だけマルチスレッドで並列処理される。<br>
Taskを呼び出した際はマルチスレッドで実行されるようになっており、連続で記述する場合は、記述した個数だけマルチスレッドで並列処理される。<br>
  <source lang="cpp">
  <source lang="c#">
  public async Task MethodAsync()
  public async Task MethodAsync()
  {
  {
     var task1 = Task.Run(() => {
     var task1 = Task.Run(() =>
            // 何か処理1
                {
            });
                  // 何か処理1
     var task2 = Task.Run(() => {
                });
            // 何か処理2
            });
     var task2 = Task.Run(() =>
     var taskAll = Task.WhenAll(task1, task2).ContinueWith(() => {
                {
            // 何か処理3
                  // 何か処理2
            });
                });
     var task12  = Task.WhenAll(task1, task2);
    var taskAll = task12.ContinueWith(() =>
                  {
                    // 何か処理3
                  });
     return await taskAll.ConfigureAwait(false);
     return await taskAll.ConfigureAwait(false);
  }
  }
  </source>
  </source>
<br>
<br><br>


== Task.WhenAllとContinueWith ==
== Task.WhenAllとContinueWith ==
26行目: 53行目:
2つ以上のスレッドを実行して全スレッドの処理が終了した後、続けてスレッドの処理を書く場合は、<br>
2つ以上のスレッドを実行して全スレッドの処理が終了した後、続けてスレッドの処理を書く場合は、<br>
ContinueWithまたはawaitを使用してスレッドの終了を待つ。<br>
ContinueWithまたはawaitを使用してスレッドの終了を待つ。<br>
  <source lang="cpp">
  <source lang="c#">
  // パターン1
  // パターン1
  var task1 = Task.Run(() =>
  var task1 = Task.Run(() =>
            {
            {
              // 何か処理
                // 何か処理
            });
            });
  var task2 = Task.Run(() =>
  var task2 = Task.Run(() =>
            {
            {
              // 何か処理
                // 何か処理
            });
            });
  var taskAll = Task.WhenAll(task1, task2).ContinueWith(task3);
var task3 = Task.Run(() =>
            {
                // 何か処理
            });
  var taskAll = Task.WhenAll(task1, task2).ContinueWith(task3); // awaitは不要
  </source>
  </source>
<br>
<br>
  <source lang="cpp">
  <source lang="c#">
  // パターン2
  // パターン2
  var task1 = Task.Run(() =>
  var task1 = Task.Run(() =>
45行目: 79行目:
                 // 何か処理
                 // 何か処理
             });
             });
  var task2 = Task.Run(() =>
  var task2 = Task.Run(() =>
             {
             {
                 // 何か処理
                 // 何か処理
             });
             });
  await task1.ConfigureAwait(false);
  await task1.ConfigureAwait(false);
  await task2.ConfigureAwait(false);
  await task2.ConfigureAwait(false);
  // task3を普通に記述
  // 以下にtask3を記述
  </source>
  </source>
<br>
<br>
状況にもよるが、処理が膨大になると可読性が落ちるので、<br>
状況にもよるが、処理が膨大になると可読性が落ちるので、<br>
先に関数で纏めて処理を記述して関数を呼ぶだけでタスク間の関係性が見通せるパターン1の方が良い。<br><br>
先に関数で纏めて処理を記述して関数を呼ぶだけでタスク間の関係性が見通せるパターン1の方が良い。<br>
<br><br>


== ConfigureAwait(false) ==
== ConfigureAwait(false) ==
taskのWaitメソッドやResultは、デフォルトで元のスレッドへ戻ろうとする。<br>
TaskのWaitメソッドやResultは、デフォルトで元のスレッドへ戻ろうとする。<br>
そのため、下記のように記述すると簡単にデッドロックが発生する。<br>
そのため、GUIアプリケーションでは、下記のように記述すると簡単にデッドロックが発生する。<br>
  <source lang="cpp">
  <source lang="c#">
  public void callMethod()
  public void callMethod()
  {
  {
66行目: 103行目:
  }
  }
   
   
  public Task methodAsync()
  public async Task methodAsync()
  {
  {
     return await Task.Delay(1000);
     return await Task.Delay(1000);
74行目: 111行目:
上記の場合、methodAsync().Wait()を呼んだ時点で親スレッドがスリープする。<br>
上記の場合、methodAsync().Wait()を呼んだ時点で親スレッドがスリープする。<br>
次に、methodAsync()が終了後、親スレッドに戻ろうとするが、既に親スレッドがスリープしているため処理を続けることができない。<br>
次に、methodAsync()が終了後、親スレッドに戻ろうとするが、既に親スレッドがスリープしているため処理を続けることができない。<br>
その結果、親スレッドは子スレッドを待ち続け、子スレッドは親スレッドに戻ろうとして、デッドロックが発生する。<br><br>
その結果、親スレッドは子スレッドを待ち続け、子スレッドは親スレッドに戻ろうとして、デッドロックが発生する。<br>
<br>
対策として、以下のように記述するとデッドロックが回避できる。(戻るスレッドはどこでもよい場合)<br>
対策として、以下のように記述するとデッドロックが回避できる。(戻るスレッドはどこでもよい場合)<br>
  <source lang="cpp">
  <source lang="c#">
  public void callMethod()
  public void callMethod()
  {
  {
82行目: 120行目:
  }
  }
   
   
  public Task waitAsync()
  public async Task waitAsync()
  {
  {
     return await Task.Delay(1000).ConfigureAwait(false);
     return await Task.Delay(1000).ConfigureAwait(false);

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

概要

.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);
 }