Exploring implicit async in C#
This post describes an imagined scenario. The examples here will not work in real life!
When async was introduced in C# it allowed us to write complex code in much cleaner syntax.
But the new syntax, although cleaner, still introduced a bit extra code to write.
A simple synchronous code could look like this.
void GenerateAndPrintResult()
{
var x = GetResult();
Console.WriteLine(x);
}
string GetResult();
The asynchronous alternative looks like this.
async Task GenerateAndPrintResult()
{
var x = await GetResultAsync();
Console.WriteLine(x);
}
Task<string> GetResultAsync();
What if we could keep the benefits of async without having to write the extra syntax.
Implicit await/async
Let’s only change the GetResult function
async void GenerateAndPrintResult()
{
var x = GetResult();
Console.WriteLine(x);
}
Task<string> GetResult();
and let the compiler generate the implicit code.
async Task GenerateAndPrintResult()
{
var x = await GetResult();
Console.WriteLine(x);
}
Every call to a function that returns
Taskwill insertawait.Every function that calls an async function will itself return
Task.
This will progress to any code calling GenerateAndPrintResult() down the call chain.
Change in signature
The problem with this solution is that for compiled libraries the signature has now changed and old code would no longer be compatible with these changes.
Introducing the async keyword
With the new implicit await we sometimes still want the Task rather than the awaited value.
For example when starting many operations at once and await them all at the end.
For this we introduce the async keyword.
void GenerateAndPrintResult()
{
var a = async GetResultA();
var b = async GetResultB();
Task.WaitAll(a, b);
Console.WriteLine(a + b);
}
Task<string> GetResultA();
Task<string> GetResultB();
Error reporting, limits to the implicit code
When the compiler converts calling functions to themselves return Task we might eventually reach a place where the conversion can’t be done automatically. For example the call is made from a library that’s not being compiled.
void Main()
{
LibraryA.Schedule(GenerateAndPrintResult);
}
void GenerateAndPrintResult()
{
var x = GetResult();
Console.WriteLine(x);
}
Task<string> GetResult();
This would generate a compiler error that must be handled.
Can't cast from Task to void
Where do we report this error?
A. At the top of the call chain, at LibraryA.Schedule, where the compiler no longer can solve the problem by the implicit inserts?
B. At every call that triggered the implicit code. In this example it’s only one function, GetResult, but it would be every function in the project.
Let’s stick with A and instead introduce a new keyword.
Introduce the sync keyword
The sync keyword would tell the compiler to stop implicit async/await/Task generation and report the error there.
sync void GenerateAndPrintResult()
{
var x = GetResult(); //← Error here
Console.WriteLine(x);
}
Task<string> GetResult();
Manually we could break the chain like this.
void GenerateAndPrintResult()
{
var x = (async GetResult()).Result;
Console.WriteLine(x);
}
Task<string> GetResult();
We could also use the sync keyword on a statement to await the result.
void GenerateAndPrintResult()
{
var x = sync GetResult();
Console.WriteLine(x);
}
Task<string> GetResult();
No you may be thinking, “aren’t we just replacing a noisy async/await with another noisy sync keyword”, and you’re probably right.
Properties
While we’re at it let’s add async support to properties.
void GenerateAndPrintResult()
{
var x = Result;
Result = "world";
Console.WriteLine(x);
}
public Task<string> Result { get; set; }
The generated code would be
async Task GenerateAndPrintResult()
{
var x = await get_Result();
await set_Result("world");
Console.WriteLine(x);
}
public Task<string> get_Result();
public Task set_Result(string value);
Comments
Send a comment by email...