C Sharpの基礎 - マルチスレッド

提供:MochiuWiki : SUSE, EC, PCB
2021年11月15日 (月) 02:21時点におけるWiki (トーク | 投稿記録)による版 (文字列「<source lang」を「<syntaxhighlight lang」に置換)
ナビゲーションに移動 検索に移動

概要

.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>型を返す非同期関数である。

<syntaxhighlight lang="c#">
public async Task<int> MethodAsync()
{
   var iRet = await Task.Run(() =>
                    {
                       await Task.Delay(5000);
                       return 0;
                    });
   return iRet;
}
</source>



asyncとawaitの動作

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

<syntaxhighlight lang="c#">
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);
}
</source>



Task.WhenAllとContinueWith

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

<syntaxhighlight lang="c#">
// パターン1
var task1 = Task.Run(() =>
            {
               // 何か処理
            });

var task2 = Task.Run(() =>
            {
               // 何か処理
            });

var task3 = Task.Run(() =>
            {
               // 何か処理
            });

var taskAll = Task.WhenAll(task1, task2).ContinueWith(task3);  // awaitは不要
</source>


<syntaxhighlight lang="c#">
// パターン2
var task1 = Task.Run(() =>
            {
               // 何か処理
            });

await task1.ConfigureAwait(false);

var task2 = Task.Run(() =>
            {
               // 何か処理
            });

await task2.ConfigureAwait(false);

var task3 = Task.Run(() =>
            {
               // 何か処理
            });

await task3.ConfigureAwait(false);
</source>


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


ConfigureAwait(false)

GUIアプリケーションでは、以下のように記述すると、簡単にデッドロックが発生する。
なぜなら、TaskのWaitメソッドやResultは、元のスレッドへ戻ろうとするからである。

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

<syntaxhighlight lang="c#">
// デッドロックが起きるサンプルコード(GUIアプリケーションのみ)
// コンソールアプリケーションでは正常に動作する
public void callMethod()
{
   methodAsync().Wait();
}

public async Task methodAsync()
{
   return await Task.Delay(1000);
}
</source>


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

<syntaxhighlight lang="c#">
// デッドロックが起きないサンプルコード(GUIアプリケーションのみ)
public void callMethod()
{
   methodAsync().Wait();
}

public async Task methodAsync()
{
   return await Task.Delay(1000).ConfigureAwait(false);
}
</source>