コマンドラインプログラムで非同期処理ってどう書くの?

awaitableconsoleprogram

… ネタが降ってきた (;´Д`)

.NETのコンソールプログラムは、Visual Studioで普通に作ると、以下のようなテンプレコードが生成されます。

namespace ConsoleApplication2
{
	internal static class Program
	{
		public static int Main(string[] args)
		{
			return 0;
		}
	}
}

勉強会のasync/awaitシリーズで説明したシチュエーションは、WPFやWinFormsという環境での話でした。そこでは、「UIキュー」が重要で、このキューに非同期処理の完了が放り込まれ、メインスレッドがこれを拾い上げて処理する事で、await以降の処理が実行されることを説明しました。

コンソールで実行されるコードの場合、この「UIキュー」が存在しません(WPFやWinFormsのインフラを使った場合は生成されますが、ここでは扱いません)。そのため、await時に「ハードウェイト」しても困らない事になります。

UIキューが存在する環境でハードウェイトすると、UIキュー内のエントリが実行されなくなります。目に見える問題として、ユーザーの操作(ウインドウを移動したりサイズ変更したり、ユーザーインターフェイス要素の操作)が無視され、アプリケーションが固まっているように見えます。

しかし、コンソールアプリケーション実行時のウインドウ「コマンドプロンプト」は、アプリケーションのスレッドの状態に関わらず、常に操作可能です。以下のProcessExplorerのスクリーンショットは、コマンドプロンプトウインドウを激しく動かしている時に撮ったものです。

awaitableconsoleprogram3

コンソールアプリケーション(ここではcmd.exe)を起動すると、同時に子プロセスとして「conhost.exe」が起動します。はっきりしたことは分かりませんが、上記のようにウインドウの操作を行うと、このconhost.exeの負荷が上昇する事から、ウインドウメッセージの処理はこのプロセスが受け持っている事が推測できます。そうであれば、実際のコンソールアプリケーションは、UIキューの処理に影響される事無く(元々持っていない)、そしてユーザーインターフェイスがブロックされる事が無く、動作するのです。

話を戻して、コンソールアプリケーションではUIキューを持っていないので、スレッドがハードブロックしても問題ありません。Taskクラスを返却する「非同期メソッド」は、必要な個所でハードブロック出来ます。この事を利用して、以下のようなスケルトンコードを考える事が出来ます。

namespace ConsoleApplication2
{
	internal static class Program
	{
		private static async Task<int> InternalMainAsync(string[] args)
		{
			// あんな非同期やこんな非同期処理 ...

			// Console.WriteLineへのアクセスはスレッドコンテキスト違反にはならない
			Console.WriteLine("async/await on Console Application.");
		}

		public static int Main(string[] args)
		{
			try
			{
				return InternalMainAsync(args).Result;
			}
			catch (Exception ex)
			{
				Console.Error.WriteLine(ex.ToString());
				return Marshal.GetHRForException(ex);
			}
		}
	}
}

Mainの最初の処理として、InternalMainAsyncメソッドを呼び出します。このメソッドはTaskクラスを返却する非同期メソッドです。そのため、このまま放置すると次の処理に行ってしまいます(Mainを抜けて終了してしまう)。そのため、Waitを呼び出してハードブロックする必要があります。上記の例ではResultプロパティを呼ぶことで、間接的にハードブロックしています(戻り値intを取得するため)。

一方、InternalMainAsyncの中の処理は全て非同期で記述できます。ここでの知識は、WPFやWinFormsで非同期処理を記述する時と全く同じ知識を活用できます。言い換えると、普段から非同期処理を書いているなら、同じ流れで書いて問題ないという事です。

スレッドはどうなるのか? ですが、メインスレッドとUIキューの関係は無く、メインスレッドはResultでハードブロックしているので、全ての非同期処理(非同期処理の継続操作)はワーカースレッドのコンテキストで駆動されます。

今話題にしているのはコンソールアプリケーションでしたね? 例えば、ワーカースレッドからConsole.WriteLineを呼び出しても問題なく出力出来ますね? 勿論、マルチスレッドアクセスが認められないクラスやメソッド、特定のスレッドコンテキストに紐づくクラス等を扱う場合は、スレッド同期やマーシャリング等の操作が必要になりますが、この辺の知識はTask.RunやThreadクラスを使って手動でワーカースレッドを作った時と同じように考えればOKです。

追記: 現代の話 (2020年)

ネタとしてはもうかなり前から出来るようになっていますが、C# 7.1以上を使用している場合は、以下のように直接MainメソッドがTaskを返すように書く事が出来ます:

namespace ConsoleApplication3
{
	internal static class Program
	{
		public static async Task<int> Main(string[] args)
		{
			// あんな非同期やこんな非同期処理 ...
		}
	}
}

ポイントは、

  • メソッド名はMainであること
  • 引数は従来通り、無しかstring[]であること
  • 戻り値の型がvoidの場合は従来通りの扱い、Task又はTask<int>の場合のみ、Mainを非同期メソッドとして認識

.NET Core/Frameworkのバージョンではなく、C#のバージョンによって使用可能になります。つまり、この機能は、コンパイラが判断して適切なコードを自動生成する事で実現しています。生成されるコードは、私たちが手動で行っていたのと同じように、Wait()やResultを呼び出してハードウェイトしている事になるため、UIキューを使うようなシチュエーションでは使えません。