Friday 15 February 2019

Async Main

Since C# 7.1 we can declare the main method of our application as async. This means that it can contain statements with the await keyword. So now we can write code like this:

static async Task Main(string[] args)
{
	Console.WriteLine("Main Started");
	var post = await GetPostAsync();
	Console.WriteLine(post);
}

In the past, we would have needed to do something like this:

static void Main(string[] args)
{
	Console.WriteLine("Main Started");
	var post = GetPostAsync().Result;
	Console.WriteLine(post);
}

The new async Main can look a bit like magic. We know that when the execution flow comes across an await it jumps out of that method, and the remaining of the method will be continued when the await action is complete. The thing is that if Main itself is async, where is the flow jumping out to?

Well, it seems that the compiler creates an additional method that becomes the real entry point to the application. It invokes the async Main and waits for it to complete, basically doing the same we were doing before C# 7.1. I've not checked it myself, but others have decompiled and posted about it.

[SpecialName]
private static void Main(string[] args)
{
	Program.Main(args).GetAwaiter().GetResult();
}

Thinking a bit more about it I've realised that there's a slight different between the "old, manual way" and the "new, compiler managed way", the thread that ends up running the continuation code. Let's see an example:

static async Task GetPost()
{
	Console.WriteLine("GetPost started - Thread: " + Thread.CurrentThread.ManagedThreadId);
	await Task.Delay(1000);
	Console.WriteLine("GetPost after first part - Thread: " + Thread.CurrentThread.ManagedThreadId);
	await Task.Delay(1000);
	Console.WriteLine("GetPost after second part - Thread: " + Thread.CurrentThread.ManagedThreadId);
	return "Post content";
}

If we invoke the above code "the old way", without async Main, all the code in the Main method is run by the same thread (it get's stopped in the .Result property access (same as calling .Wait()), and then continues)

static void Main(string[] args)
{
	Console.WriteLine("Main Started - Thread: " + Thread.CurrentThread.ManagedThreadId);
	var post = GetPost().Result;
	Console.WriteLine("Main after GetPost - Thread: " + Thread.CurrentThread.ManagedThreadId);
	Console.WriteLine("post: " + post);
}


// Main Started - Thread: 1
// GetPost started - Thread: 1
// GetPost after first part - Thread: 4
// GetPost after second part - Thread: 4
// Main after GetPost - Thread: 1
// post: Post content

However, if we invoke the same method from an "async Main", the second part of the Main, the one after await is run by a thread different from the one that started the Main (notice that the starting thread is blocked in the "hidden" GetAwaiter().GetResult()).

static async Task Main(string[] args)
{
	Console.WriteLine("Main Started - Thread: " + Thread.CurrentThread.ManagedThreadId);
	var post = await GetPost();
	Console.WriteLine("Main after GetPost - Thread: " + Thread.CurrentThread.ManagedThreadId);
	Console.WriteLine("post: " + post);
}
//Main Started - Thread: 1
//GetPost started - Thread: 1
//GetPost after first part - Thread: 4
//GetPost after second part - Thread: 4
//Main after GetPost - Thread: 4
//post: Post content	
		

For the most part whether that code runs in one thread or another is of no particular concern, but I can think of at least one case where it matters, when impersonation is being used.

No comments:

Post a Comment