C# Asynchronous Invocation

Asynchronous method is implemented in C# by using async/await keywords.

Async/Await keywords

That’s the goal of asynchronous: enable code that reads/writes like a sequence of statements, but executes in a much more complicated order based on external resource allocation and when tasks complete.

Without language support, writing asynchronous code required callbacks, completion events, or other means that obscured the original intent of the code. In C#, keywords async and await are used to achieved a asynchronous program. The function return immediately when it meets a await. A code sample is like following.

[VoidReturnAsyncMethods] [CSharp]view raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using System;
using System.Threading.Tasks;


namespace TestAsyncIL
{
class TestAsync
{
public async void Method1()
{
Console.WriteLine("Start Method 1");
for (int i = 0; i < 3; i++)
{
await Task.Delay(330);
Console.WriteLine("Method 1: " + i.ToString());
}
}

public async void Method2()
{
Console.WriteLine("Start Method 2");
for (int i = 0; i < 3; i++)
{
await Task.Delay(330);
Console.WriteLine("Method 2: " + i.ToString());
}
}

public void Method3()
{
Console.WriteLine("Start Method 3");
for (int i = 0; i < 3; i++)
{
Console.WriteLine("Method 3: " + i.ToString());
}
}
}

class Program
{
static void Main(string[] args)
{
TestAsync testAsync = new TestAsync();
testAsync.Method1();
testAsync.Method2();
testAsync.Method3();
Console.Read();
/* Output is (replaced '\n' by ',')
Start Method 1, Start Method 2, Start Method 3
Method 3: 0, Method 3: 1, Method 3: 2
Method 1: 0, Method 2: 0, Method 1: 1
Method 2: 1, Method 1: 2, Method 2: 2
*/
}
}
}

How to code executed:

  1. Run Method1, code output method1, then encounter await, get back to the main function
  2. Run Method2, code output method2, then encounter await, get back to the main function
  3. Run Method3, since the running time is less than 330 milliseconds, it will output all the content in Method3
  4. After 330 milliseconds from Method1 returning, Method1 output the next str
  5. After 330 milliseconds from Method2 returning, Method2 output the next str
  6. Repeat 4 and 5 until output all string.

Note. the step 4 and 5 won’t output if we don’t add the ReadKey method in the end because the main thread will ends and those asnyc methods still in the same thread thus end, too.

Analysis of metadata

In IL, we find that the compiler generate 2 classes for Method1 and Method2 called <Method1>d__0 and <Method2>d__1. The two classes are state machines, the implement the interface TStateMachine. The class contains the following elements.

  • Fields
    • int state
    • TestAsync this
    • AsyncVoidMethodBuilder builder
    • TaskAwaiter u__1
    • int 5__1 (the local int i in for loop)
  • Methods
    • .ctor
    • MoveNext
    • SetStateMachine

There are two important data types. System.Runtime.CompilerServices.AsyncVoidMethodBuilder and System.Runtime.CompilerServices.TaskAwaiter

  • AsyncVoidMethodBuilder
    • A struct, value type
    • Represents a builder for asynchronous methods that do not return a value.
    • Its Create method will return a instance of the builder
    • Its Start<TStateMachine> method will begin running the builder with the associated state machine
  • TaskAwaiter
    • A struct, value type
    • Provides an object that can check whether current asynchronous task is completed and waits for the completion of an asynchronous task
    • Properties
      • IsCompleted. Gets a value that indicates whether the asynchronous task has completed.
    • Methods
      • GetResult. Ends the wait for the completion of the asynchronous task.
      • OnCompleted(Action). Sets the action to perform when the TaskAwaiter object stops waiting for the asynchronous task to complete.
      • UnsafeOnCompleted(Action). Schedules the continuation action for the asynchronous task that is associated with this awaiter.

IL execution pipeline

For the Method2 itself, the IL code is as follows

Once we call the Method2, it does the following things.

  1. Initializes a class called <Method2>d__1.
  2. Assign the current instance to <Method2>d__1::this
  3. Call AsyncVoidMethodBuilder::Create<<Method2>d__1> to create a async void instance for the state machine
  4. Set <Method2>d__1::state to -1
  5. Call AsyncVoidMethodBuilder.Start, the passing state machine is the class <Method2>d__1
  6. Call the <Method2>d__1::MoveNext, since the state is -1, output the str, and change state to 0
  7. Once the state is set to 0, initialize the 5__1 field as 0, then call the Task.Delay method, and get awaiter for this task and set it to u__1 field
  8. Check u__1::IsCompleted. If haven’t finish, call AsyncVoidMethodBuilder::AwaitUnsafeOnCompleted<5__1, <Method2>d__1>, which will call <Method2>d__1::MoveNext method when the status of the awaiter is complete
    • Note. I think the AsyncVoidMethodBuilder will set the <Method2>d__1::MoveNext to 5__1::UnsafeOnCompleted(Action)
  9. The next time it call the MoveNext method, the code will execute different logic since the state has been changed.

Asynchronous method return types

Async methods can have the following return types

| void | Task | Task<TResult>
—–|——|——|—————
has return statement | No | Yes | Yes
return statement type | - | Task | Task<TResult>
can assign to delegate or event | Yes | Yes | Yes
can assign to Task | No | Yes | Yes
caller can know whether this method has completed (create a awaiter for it) | No | Yes | Yes
its awaiter has return statement | - | No | Yes
its awaiter return type | - | - | TResult

Task and Task class

Compared to execute a thread to do actions, the Task class is used to execute asynchronously, and it can track the status of this asynchronous action, including whether it finish and what is the return value. Most commonly, a lambda expression is used to specify the work that the task is to perform.

For operations that return statement, the Task<TResult> class will be used.

Samples

[TaskReturnAsyncMethods] [CSharp]view raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
using System;
using System.Threading.Tasks;


namespace TestAsyncIL
{
struct ReturnArgs
{
public int Data { get; set; }
public string Text { get; set; }
}

class TestAsyncTask
{
public async Task Method1()
{
Console.WriteLine("Start Method 1");
for (int i = 0; i < 3; i++)
{
await Task.Delay(330);
Console.WriteLine("Method 1: " + i.ToString());
}
}

public async Task<ReturnArgs> Method2()
{
Console.WriteLine("Start Method 2");
for (int i = 0; i < 3; i++)
{
await Task.Delay(330);
Console.WriteLine("Method 2: " + i.ToString());
}
return new ReturnArgs() { Data = 2, Text = "Task Finished!" };
}

public void Method3()
{
Console.WriteLine("Start Method 3");
for (int i = 0; i < 3; i++)
{
Console.WriteLine("Method 3: " + i.ToString());
}
}
}

class Program
{
static void Main(string[] args)
{
// mMain1();
// mMain2();
mMain3();
Console.ReadKey();
}

static async void mMain1()
{
TestAsyncTask testAsync = new TestAsyncTask();
testAsync.Method1();
testAsync.Method2();
testAsync.Method3();
/* Output is (replaced '\n' by ',')
Start Method 1, Start Method 2, Start Method 3
Method 3: 0, Method 3: 1, Method 3: 2
Method 1: 0, Method 2: 0, Method 1: 1
Method 2: 1, Method 1: 2, Method 2: 2
*/
}

static async void mMain2()
{
TestAsyncTask testAsync = new TestAsyncTask();
await testAsync.Method1();
ReturnArgs args = await testAsync.Method2();
Console.WriteLine(args.Data.ToString() + " " + args.Text);
testAsync.Method3();
/* Output is (replaced '\n' by ',')
Start Method 1, Method 1: 0, Method 1: 1, Method 1: 2
Start Method 2, Method 2: 0, Method 2: 1, Method 2: 2
2 Task Finished!
Start Method 3, Method 3: 0, Method 3: 1, Method 3: 2
*/
}

static async void mMain3()
{
TestAsyncTask testAsync = new TestAsyncTask();
Task a = testAsync.Method1();
Task<ReturnArgs> b = testAsync.Method2();
Task.WaitAny(a);
var args = await b;
Console.WriteLine(args.Data.ToString() + " " + args.Text);
testAsync.Method3();
/* Output is (replaced '\n' by ',')
Start Method 1, Start Method 2
Method 1: 0, Method 2: 0,
Method 1: 1, Method 2: 1,
Method 1: 2, Method 2: 2,
2 Task Finished!
Start Method 3, Method 3: 0, Method 3: 1, Method 3: 2
*/
}
}
}

In the mMain1 method, the Method1 and Method2 are still be executed in a synchronously. So the output is the same with the void-returning async method.

In the mMain2 method, we create the awaiter for the Method1 and Method2, so Method2 will be called after Method1, and Method3 will be called after Method2.

In the mMain3 method, we firstly run the Method1 and Method2 synchronously and assign them to Task and Task<TResult> variables. So the two methods will run at the same time. Then we use Task.WaitAny and create a awaiter for Method2, so Method3 will run after Method1 and Method2.

Analysis in IL

Similarly, the complier will generate 2 state machine classes for Method1 and Method2 and 3 state machine for mMain1, mMain2, mMain3.

In the state machine of Method1 and Method2, it is similar to the void-returning async method but it uses AsyncTaskMethodBuilder<TResult> classes to build the async task. In Method2, when it is finished, it uses SetResult(TResult) method to marks the task as successfully completed and also pass the ReturnArgs statement.

In the mMain2 and mMain3 state machines, there will be TaskAwaiter class and TaskAwaiter<TResult> result. After the awaiter’s property IsCompleted is true, they will call TaskAwaiter<TResult>.GetResult to get the return statement from the AsyncTaskMethodBuilder<TResult>.

Cancellation token to terminate async thread

The Async function can be cancelled by passing a CancellationToken parameter created by a CancellationTokenSource.

[AsyncWithToken] [CSharp]view raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
CancellationTokenSource tokenSource;

/// Upload new anchor pipeline
public void UploadNewAnchor()
{
// Create a new token because token can only by cancelled once
tokenSource = new CancellationTokenSource();

// Add the callback methods when the token is cancelled
tokenSource.Token.Register(() => feedbackBox.text = "Cancel Action!");
tokenSource.Token.Register(async () =>
{
CloudManager.StopSession();
await cloudManager.ResetSessionAsync();
});

// Start the async method
UploadNewAnchorAsync(tokenSource.Token);
}

/// Upload anchor process, can be canceled anytime
async void UploadNewAnchorAsync(CancellationToken token)
{
while (!CloudManager.IsReadyForCreate)
{
await Task.Delay(330);
token.ThrowIfCancellationRequested();
}

CloudManager.UploadAnchor();
}

/// Stop the uploading process by the cancellation token
public void StopAction()
{
if (tokenSource != null)
{
tokenSource.Cancel();
tokenSource.Dispose();
}
}

Reference

  1. AsyncVoidMethodBuilder Struct
  2. AsyncTaskMethodBuilder Struct
  3. AsyncTaskMethodBuilder Struct
  4. TaskAwaiter Struct
  5. TaskAwaiter.GetResult Method
  6. Task.GetAwaiter Method
  7. Task.Delay Method