C Sharpの基礎 - マルチスレッド
概要
.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);
}
Wait / Result
コンソールソフトウェアにおいて、Wait
メソッドおよびResult
メソッドを使用して、非同期メソッドを作成することができる。
HttpClient hc = new HttpClient();
string html = hc.GetStringAsync("http://example.jp/").Result;
また、Wait
メソッドおよびResult
メソッド以外に、WaitAll
メソッドやWaitAny
メソッドが存在する。
HttpClient hc = new HttpClient();
Task<string> t1 = hc.GetStringAsync("https://www.microsoft.com/");
Task<string> t2 = hc.GetStringAsync("https://www.bing.com/");
// タスクが終わるまでスレッドをブロック
t1.Wait();
// タスクが終わるまでスレッドをブロックして結果を取得
string binghtml = t2.Result;
// どれかのタスクが終わるまでスレッドをブロック
int completedTaskIndex = Task.WaitAny(t1, t2);
// 0 : タスクt1の方が速い場合
// 1 : タスクt2の方が速い場合
// どれかのタスクが終わるまでスレッドをブロック(タイムアウトあり)
int completedTaskIndex2 = Task.WaitAny(new[] { t1, t2 }, 50);
// 0 : タスクt1の方が速い場合
// 1 : タスクt2の方が速い場合
// -1 : タスクt1およびt2とも50[ms]以内に応答がない場合
// 全てタスクが終わるまでスレッドをブロック
Task.WaitAll(t1, t2);
// 全てのタスクが終わるまでスレッドをブロック(タイムアウトあり)
bool allTasksCompleted = Task.WaitAll(new[] { t1, t2 }, 50);
// true : タスクt1およびt2とも50[ms]以内に応答がある場合
// false : タスクt1またはt2のいずれか一方、または、タスクt1およびt2とも50[ms]以内に応答がない場合
WhenAny
複数のタスクのうち、いずれか1つのみが完了するまで待機する場合、WhenAny
メソッドを使用する。
HttpClient hc = new HttpClient();
Task<string> t1 = hc.GetStringAsync("https://www.microsoft.com/");
Task<string> t2 = hc.GetStringAsync("https://www.bing.com/");
Task<string> completedTask = await Task.WhenAny(t1, t2);
これを応用して、処理継続中のタスクをキャンセルすることもできる。
HttpClient hc = new HttpClient();
CancellationTokenSource cts = new CancellationTokenSource();
// GetStringAsyncメソッドにはCancellationToken構造体を受けるオーバーロードが無いため、GetAsyncメソッドを使用している
Task<HttpResponseMessage> t1 = hc.GetAsync("https://www.microsoft.com/", cts.Token);
Task<HttpResponseMessage> t2 = hc.GetAsync("https://www.bing.com/", cts.Token);
// 完了したタスクを取得
Task<HttpResponseMessage> completedTask = await Task.WhenAny(t1, t2);
// 他の処理中のタスクは全てキャンセルする
cts.Cancel();
// 完了したタスクの結果を取得
HttpResponseMessage msg = await completedTask;
string html = await msg.Content.ReadAsStringAsync();
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(() =>
{
// 何か処理
});
await task1.ConfigureAwait(false);
var task2 = Task.Run(() =>
{
// 何か処理
});
await task2.ConfigureAwait(false);
var task3 = Task.Run(() =>
{
// 何か処理
});
await task3.ConfigureAwait(false);
状況にもよるが、処理が膨大になると可読性が落ちるため、先にタスクをまとめて記述して、タスク間の関係性が見通せるパターン1の方が良い。
IsCompletedプロパティ
タスクが完了しているかどうかを確認する場合、IsCompleted
プロパティを使用する。
IsCompletedSuccessfully
プロパティもあるが、これはタスクが成功および完了した場合のみtrue
になる。
また、IsFaulted
プロパティやIsCanceled
プロパティも存在する。
HttpClient hc = new HttpClient();
Task<string> task = hc.GetStringAsync("https://www.microsoft.com/");
// タスクが成功および完了したかどうかを確認
if (!task.IsCompleted)
{
// この時点でタスクt1が成功および完了している場合、このメッセージは表示されない
Console.WriteLine("ちょっとまってね");
}
var html = await task;
Console.WriteLine(html);
ConfigureAwait(false)
GUIアプリケーションにおいて、Wait
メソッドおよびResult
メソッドを使用する場合、デッドロックが発生する。
これは、Task
クラスのWait
メソッドやResult
メソッドは、元のスレッドへ戻るからである。
以下の例では、まず、methodAsync().Wait()を実行した時点で親スレッドがスリープする。
次に、methodAsyncメソッドの完了後、親スレッドに戻る時、既に親スレッドがスリープしているため、処理を続けることができない。
そのため、親スレッドは子スレッドを待ち続け、子スレッドは親スレッドに戻ろうとして、デッドロックが発生する。
// デッドロックが起きるサンプルコード(GUIアプリケーションのみ)
// コンソールアプリケーションでは正常に動作する
public void callMethod()
{
methodAsync().Wait();
}
public async Task methodAsync()
{
return await Task.Delay(1000);
}
対策として、ConfigureAwait(false)
を使用するとデッドロックが回避できる。
ConfigureAwait(false)
は、メソッドの終了後に親スレッド(呼び出し元)に戻らなくてもよいことを指定する。
ブロックされたスレッドに戻らなくなるため、methodAsyncメソッドが終了した時にcallMethodメソッドに戻ることができる。
// デッドロックが起きないサンプルコード(GUIアプリケーションのみ)
public void callMethod()
{
methodAsync().Wait();
}
public async Task methodAsync()
{
return await Task.Delay(1000).ConfigureAwait(false);
}