こんにちはー、ニアです!
本記事では、C# 5.0で追加された async / await とTask.Runメソッドを用いた非同期処理の流れをメモとして載せています。
※元々この記事は、C#における非同期処理の健忘帳として書いていましたが、この度はコンソールアプリとGUIアプリにおける処理の流れの違いを図も併せてまとめ、記事をリニューアルすることにしました。
1. 非同期処理の作り方
- 非同期にしたい処理をTask.Runメソッドの引数にデリーゲートとして指定します。
- Task.Runメソッドにawaitキーワードを付けて、非同期処理が終わるまで後続の処理を待機するようにします。
- await付きのメソッドを含むメソッドにはasyncキーワードを付けて、戻り値をTask型にします。
※非同期処理にて、async付きのメソッドの戻り値をTask型にする理由は以下のサイトが参考になります。
asyncの落とし穴Part3, async voidを避けるべき100億の理由 – neue cc
2. コンソールアプリの場合
まずは、コンソールアプリでの動作の流れを追ってみます。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication1 {
class Program {
static void Main( string[] args ) {
Console.WriteLine( $"{nameof( Main )} : 1 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
Task a = SumAsync( 120000000 );
Console.WriteLine( $"{nameof( Main )} : 2 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
// SumAsyncメソッドの処理が終わるまで待機します。
a.Wait();
Console.WriteLine( $"{nameof( Main )} : 3 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
}
// 1~nの各逆数の総和を計算
static double Sum( int n ) {
Console.WriteLine( $"{nameof( Sum )} : 1 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
double ans = 0;
while( n > 0 ) {
ans += 1.0 / n--;
}
Console.WriteLine( $"{nameof( Sum )} : 2 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
return ans;
}
static async Task SumAsync( int n ) {
Console.WriteLine( $"{nameof( SumAsync )} : 1 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
// 非同期処理を実行します。
double sum = await Task.Run( () => Sum( n ) );
Console.WriteLine( $"{nameof( SumAsync )} : 2 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
Console.WriteLine( $"Sum( 1 / 1 to 1 / {n} ) = {sum}" );
Console.WriteLine( $"{nameof( SumAsync )} : 3 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
}
}
}
Main : 1 -> ID : 1
SumAsync : 1 -> ID : 1
Sum : 1 -> ID : 3
Main : 2 -> ID : 1
Sum : 2 -> ID : 3
SumAsync : 2 -> ID : 3
Sum( 1 / 1 to 1 / 120000000 ) = 19.1802179698137
SumAsync : 3 -> ID : 3
Main : 3 -> ID : 1
スレッドのIDを見てみると、SumAsyncメソッド内では「await Task.Run( () => Sum( n ) )」を実行するまでは呼び出し元(Mainメソッド)と同じスレッドであり、Sumメソッド内及び「await Task.Run( () => Sum( n ) )」の後続処理では別のスレッドで処理していることが分かります。
3. GUIアプリの場合
次に、GUIアプリでの動作の流れを追ってみます。
本記事では、例としてWPFアプリで配置したボタンを押した時のイベントにて、非同期処理の動作をテストしてみました。なお、コンソールアプリにできるだけ合わせるために、出力先をデバッグコンソールに指定し、15行目でSumAsyncメソッドの処理が終わるまで、16行目のDebug.WriteLineメソッドの呼び出しを待機させています。
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace WpfApplication1 {
public partial class MainWindow : Window {
// 中略
private async void button_Click( object sender, RoutedEventArgs e ) {
Debug.WriteLine( $"Event : 1 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
Task a = SumAsync( 120000000 );
Debug.WriteLine( $"Event : 2 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
// SumAsyncメソッドの処理が終わるまで待機します。
await a;
Debug.WriteLine( $"Event : 3 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
}
// 1~nの各逆数の総和を計算
static double Sum( int n ) {
Debug.WriteLine( $"{nameof( Sum )} : 1 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
double ans = 0;
while( n > 0 ) {
ans += 1.0 / n--;
}
Debug.WriteLine( $"{nameof( Sum )} : 2 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
return ans;
}
static async Task SumAsync( int n ) {
Debug.WriteLine( $"{nameof( SumAsync )} : 1 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
double sum = await Task.Run( () => Sum( n ) );
// 非同期処理を実行します。
Debug.WriteLine( $"{nameof( SumAsync )} : 2 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
Debug.WriteLine( $"Sum( 1 / 1 to 1 / {n} ) = {sum}" );
Debug.WriteLine( $"{nameof( SumAsync )} : 3 -> ID : {Thread.CurrentThread.ManagedThreadId}" );
}
}
}
Event : 1 -> ID : 10
SumAsync : 1 -> ID : 10
Event : 2 -> ID : 10
Sum : 1 -> ID : 6
Sum : 2 -> ID : 6
SumAsync : 2 -> ID : 10
Sum( 1 / 1 to 1 / 120000000 ) = 19.1802179698137
SumAsync : 3 -> ID : 10
Event : 3 -> ID : 10
スレッドのIDを見てみると、SumAsyncメソッド内では「await Task.Run( () => Sum( n ) )」を実行するまではUIのスレッドであり、Sumメソッド内では別のスレッドで処理しているのはコンソールアプリと似ていますが、「await Task.Run( () => Sum( n ) )」の後からSumAsyncメソッドの終わりまではコンソールアプリとは異なり、UIのスレッドで処理していることが分かります。
4. コンソールアプリとGUIアプリで、await付きメソッド前後でスレッドIDが異なっているのはなぜ?
なぜ、await付きのメソッド前後でスレッドIDが、コンソールアプリとGUIアプリで異なるのかというと、それはSynchronizationContext.Currentに設定されているコンテキストが異なるからです。
GUIアプリの場合、UIの更新は必ずUIスレッドで行う必要があるため、SynchronizationContext.CurrentにUIスレッドで行う処理などの情報が格納されています。await付きメソッドの後続処理では、SynchronizationContext.Currentに関連づけられたスレッドで実行されるので、GUIアプリの場合、UIスレッドで実行されます。
一方、コンソールアプリはというと・・・、
コンソール アプリは同期コンテキストを持っていません。SynchronizationContext.Currentはnullです。
async/awaitと同時実行制御 – ++C++; // 未確認飛行 C ブログより引用
この場合、「awaitは既定の同期コンテキストを使う」ということになっていて、その既定の同期コンテキストはスレッド プールを使います。
Task.Runメソッドで指定したデリゲートはスレッドプール上で実行されるので、await付きメソッドの後続処理はスレッドプール上のスレッドすなわち、Task.Runメソッドで生成したスレッドで実行されると考えられます。
[END]
コメント