[C#5.0~] async/awaitとTask.Runメソッドを用いた非同期処理のメモ

こんにちはー、ニアです!

本記事では、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}" );
		}
	}
}
Nia-TN-SDfs-normal2.png
コンソールアプリでは、Mainメソッドを抜けるとアプリ自体が終了してしまうので、12行目でSumAsyncの処理が終わるまで待機させています。
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スレッドで実行されます。

Nia-TN-SDfs-normal2.png
非同期処理の結果をUIに反映するというシチュエーションは、よくあるよ~

一方、コンソールアプリはというと・・・、

コンソール アプリは同期コンテキストを持っていません。SynchronizationContext.Currentはnullです。
この場合、「awaitは既定の同期コンテキストを使う」ということになっていて、その既定の同期コンテキストはスレッド プールを使います。

async/awaitと同時実行制御 – ++C++; // 未確認飛行 C ブログより引用

Task.Runメソッドで指定したデリゲートはスレッドプール上で実行されるので、await付きメソッドの後続処理はスレッドプール上のスレッドすなわち、Task.Runメソッドで生成したスレッドで実行されると考えられます。

[END]

コメント

タイトルとURLをコピーしました