文章目录
  1. 1. 前言
  2. 2. 异步编程
    1. 2.1. Task-based Asynchronous Pattern(TAP)
    2. 2.2. 注意事项
    3. 2.3. Task
      1. 2.3.1. Task基础使用
      2. 2.3.2. Task使用Async和Await实现异步方法定义和等待
      3. 2.3.3. Task异步取消
      4. 2.3.4. Task总结
        1. 2.3.4.1. 深入理解 AsyncTaskMethodBuilder
        2. 2.3.4.2. 异步方法的生命周期
        3. 2.3.4.3. 总结
      5. 2.3.5. 使用体验
    4. 2.4. 自定义Task
    5. 2.5. 进阶(UniTask)
  3. 3. 重点知识
  4. 4. Reference
  5. 5. Github

前言

游戏开发过程中为了无论资源加载异步还是逻辑异步,异步都是不可或缺的一部分。本章节是为了深入理解C#的异步编程以及Unity里异步编程使用而编写。

异步编程

Unity里最初的模拟异步编程概念的当之无愧是携程,但携程实际上是单线程通过每帧等待实现异步模拟运行的而非真正的异步编程。

异步编程的过程中我们往往会采用回调式的方式来编写代码,这回导致回调地狱,导致代码理解起来比较困难,有了Task异步编程我们可以实现类似同步式的代码编写来编写异步代码。

而本章节的重点是真正深入了解异步编程,C#里的最新的异步编程主要是通过Task

Task-based Asynchronous Pattern(TAP)

The core of async programming is the Task and Task objects, which model asynchronous operations. They are supported by the async and await keywords.

TAP uses a single method to represent the initiation and completion of an asynchronous operation.

从上面的介绍可以看出异步编程的核心是Task实现了异步操作,并且支持async和await关键词,其次TAP通过方法定义就完成了所有的异步操作初始化和方法定义。

注意事项

  1. await can only be used inside an async method.(await只能用在async标记的方法内)
  2. async methods need to have an await keyword in their body or they will never yield!(async标记的方法需要一个await关键词在方法定义内,不然这个异步方法不会暂停会同步执行)

Task

The Task class represents a single operation that does not return a value and that usually executes asynchronously. Task objects are one of the central components of the task-based asynchronous pattern first introduced in the .NET Framework 4. Because the work performed by a Task object typically executes asynchronously on a thread pool thread rather than synchronously on the main application thread, you can use the Status property, as well as the IsCanceled, IsCompleted, and IsFaulted properties, to determine the state of a task.

从介绍可以看出,异步编程主要是通过Task类的抽象,而Task的异步编程底层伴随着线程池,我们使用的人可以不用关心底层线程池的优化调度问题。通过Task抽象异步编程,Task可以返回异步运行状态IsCanceled,IsCompleted,IsFaulted等来查看异步运行状态。

通过实战使用,了解下Task的基础使用以及底层跨线程的设计:

Task基础使用

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
/// <summary>
/// 响应基础Task使用按钮点击
/// </summary>
public void OnBtnBasicTaskUsingClick()
{
// 创建一个Task1但不开始
Task task1 = new Task(mTaskActionDelegate, "Task1");
// 通过Task工厂创建一个Task2并等待完成
Task task2 = Task.Factory.StartNew(mTaskActionDelegate, "Task2");
task2.Wait();
// Task1开始
task1.Start();
Debug.Log($"Task1 has been launched. Main Thread:{Thread.CurrentThread.ManagedThreadId}");
// 等待Task1完成
task1.Wait();
// 通过Task.Run创建Task3并等待完成
Task task3 = Task.Run(()=>
{
Debug.Log($"ActionName:Task3");
Debug.Log($"Task:{Task.CurrentId} Thread:{Thread.CurrentThread.ManagedThreadId}");
});
Debug.Log($"Task3 has been launched. Main Thread:{Thread.CurrentThread.ManagedThreadId}");
task3.Wait();
}

/// <summary>
/// Task使用方法
/// </summary>
/// <param name="actionName"></param>
private void TaskAction(object actionName)
{
Debug.Log($"ActionName:{actionName}");
Debug.Log($"Task:{Task.CurrentId} Thread:{Thread.CurrentThread.ManagedThreadId}");
}

TaskBasicUsing

从上面的Task尝试可以看出,Task是在不同线程开启的,其次Task如果是单独的New是不会自动开始的,Task.Run和Task.Factory.StartNew才会直接开始一个Task。

Task使用Async和Await实现异步方法定义和等待

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
/// <summary>
/// 响应Task等待按钮点击
/// </summary>
private async void OnBtnAwaitTaskUsingClick()
{
var awaitTask = Task.Run(AwaitTaskAction);
Debug.Log($"AwaitTaskAction has been launched. Main Thread:{Thread.CurrentThread.ManagedThreadId}");
var sumResult = await awaitTask;
Debug.Log($"sumResult:{sumResult}");
Debug.Log($"DateTime.Now.Millisecond:{DateTime.Now.Millisecond}");
}

/// <summary>
/// 等待的Task方法
/// </summary>
/// <returns></returns>
private async Task<int> AwaitTaskAction()
{
Debug.Log($"AwaitTaskAction Start");
Debug.Log($"DateTime.Now.Millisecond:{DateTime.Now.Millisecond}");
await Task.Delay(1000);
Debug.Log($"AwaitTaskAction After Task.Delay(1)");
Debug.Log($"DateTime.Now.Millisecond:{DateTime.Now.Millisecond}");
int sum = 0;
for(int i = 0; i < 1000000000; i++)
{
sum++;
}
await Task.Delay(2000);
Debug.Log($"AwaitTaskAction End");
Debug.Log($"DateTime.Now.Millisecond:{DateTime.Now.Millisecond}");
return sum;
}

AwaitTaskUsing

通过Async关键词,我把OnBtnAwaitTaskUsingClick和AwaitTaskAction方法都定义成了异步方法(既可以通过Task异步调用的方法)。然后结合Await关键词实现了等待异步方法AwaitTaskAction执行完后返回运算结果并打印的效果,可以看到这样一来我们的异步代码编写结合Await关键词就跟同步代码一般是一个线性流程,同时异步方法内通过Task.Delay等类似携程的等待方法实现指定时间指定条件的等待。

那么Async和Await关键词是如何实现异步等待效果的了,让我们结合反编译看一下底层实现:

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
[AsyncStateMachine(typeof (GameLauncher.<OnBtnAwaitTaskUsingClick>d__9))]
[DebuggerStepThrough]
private void OnBtnAwaitTaskUsingClick()
{
GameLauncher.<OnBtnAwaitTaskUsingClick>d__9 stateMachine = new GameLauncher.<OnBtnAwaitTaskUsingClick>d__9();
stateMachine.<>4__this = this;
stateMachine.<>t__builder = AsyncVoidMethodBuilder.Create();
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start<GameLauncher.<OnBtnAwaitTaskUsingClick>d__9>(ref stateMachine);
}

[AsyncStateMachine(typeof (GameLauncher.<AwaitTaskAction>d__10))]
[DebuggerStepThrough]
private Task<int> AwaitTaskAction()
{
GameLauncher.<AwaitTaskAction>d__10 stateMachine = new GameLauncher.<AwaitTaskAction>d__10();
stateMachine.<>4__this = this;
stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start<GameLauncher.<AwaitTaskAction>d__10>(ref stateMachine);
return stateMachine.<>t__builder.Task;
}

[CompilerGenerated]
private sealed class <OnBtnAwaitTaskUsingClick>d__9 : IAsyncStateMachine
{
public int <>1__state;
public AsyncVoidMethodBuilder <>t__builder;
public GameLauncher <>4__this;
private Task<int> <awaitTask>5__1;
private int <sumResult>5__2;
private int <>s__3;
private TaskAwaiter<int> <>u__1;

public <OnBtnAwaitTaskUsingClick>d__9()
{
base..Ector();
}

void IAsyncStateMachine.MoveNext()
{
int num1 = this.<>1__state;
try
{
TaskAwaiter<int> awaiter;
int num2;
if (num1 != 0)
{
this.<awaitTask>5__1 = Task.Run<int>(new Func<Task<int>>((object) this.<>4__this, __methodptr(AwaitTaskAction)));
Debug.Log((object) string.Format("AwaitTaskAction has been launched. Main Thread:{0}", (object) Thread.CurrentThread.ManagedThreadId));
awaiter = this.<awaitTask>5__1.GetAwaiter();
if (!awaiter.IsCompleted)
{
this.<>1__state = num2 = 0;
this.<>u__1 = awaiter;
GameLauncher.<OnBtnAwaitTaskUsingClick>d__9 stateMachine = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, GameLauncher.<OnBtnAwaitTaskUsingClick>d__9>(ref awaiter, ref stateMachine);
return;
}
}
else
{
awaiter = this.<>u__1;
this.<>u__1 = new TaskAwaiter<int>();
this.<>1__state = num2 = -1;
}
this.<>s__3 = awaiter.GetResult();
this.<sumResult>5__2 = this.<>s__3;
Debug.Log((object) string.Format("sumResult:{0}", (object) this.<sumResult>5__2));
Debug.Log((object) string.Format("DateTime.Now.Millisecond:{0}", (object) DateTime.Now.Millisecond));
}
catch (Exception ex)
{
this.<>1__state = -2;
this.<awaitTask>5__1 = (Task<int>) null;
this.<>t__builder.SetException(ex);
return;
}
this.<>1__state = -2;
this.<awaitTask>5__1 = (Task<int>) null;
this.<>t__builder.SetResult();
}

[DebuggerHidden]
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
}
}

[CompilerGenerated]
private sealed class <AwaitTaskAction>d__10 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder<int> <>t__builder;
public GameLauncher <>4__this;
private int <sum>5__1;
private int <i>5__2;
private TaskAwaiter <>u__1;

public <AwaitTaskAction>d__10()
{
base..Ector();
}

void IAsyncStateMachine.MoveNext()
{
int num1 = this.<>1__state;
int sum51;
try
{
TaskAwaiter awaiter1;
int num2;
TaskAwaiter awaiter2;
if (num1 != 0)
{
if (num1 != 1)
{
Debug.Log((object) "AwaitTaskAction Start");
Debug.Log((object) string.Format("DateTime.Now.Millisecond:{0}", (object) DateTime.Now.Millisecond));
awaiter1 = Task.Delay(1000).GetAwaiter();
if (!awaiter1.IsCompleted)
{
this.<>1__state = num2 = 0;
this.<>u__1 = awaiter1;
GameLauncher.<AwaitTaskAction>d__10 stateMachine = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, GameLauncher.<AwaitTaskAction>d__10>(ref awaiter1, ref stateMachine);
return;
}
}
else
{
awaiter2 = this.<>u__1;
this.<>u__1 = new TaskAwaiter();
this.<>1__state = num2 = -1;
goto label_12;
}
}
else
{
awaiter1 = this.<>u__1;
this.<>u__1 = new TaskAwaiter();
this.<>1__state = num2 = -1;
}
awaiter1.GetResult();
Debug.Log((object) "AwaitTaskAction After Task.Delay(1)");
DateTime now = DateTime.Now;
Debug.Log((object) string.Format("DateTime.Now.Millisecond:{0}", (object) now.Millisecond));
this.<sum>5__1 = 0;
for (this.<i>5__2 = 0; this.<i>5__2 < 1000000000; ++this.<i>5__2)
++this.<sum>5__1;
awaiter2 = Task.Delay(2000).GetAwaiter();
if (!awaiter2.IsCompleted)
{
this.<>1__state = num2 = 1;
this.<>u__1 = awaiter2;
GameLauncher.<AwaitTaskAction>d__10 stateMachine = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, GameLauncher.<AwaitTaskAction>d__10>(ref awaiter2, ref stateMachine);
return;
}
label_12:
awaiter2.GetResult();
Debug.Log((object) "AwaitTaskAction After Task.Delay(2)");
now = DateTime.Now;
Debug.Log((object) string.Format("DateTime.Now.Millisecond:{0}", (object) now.Millisecond));
Debug.Log((object) "AwaitTaskAction End");
now = DateTime.Now;
Debug.Log((object) string.Format("DateTime.Now.Millisecond:{0}", (object) now.Millisecond));
sum51 = this.<sum>5__1;
}
catch (Exception ex)
{
this.<>1__state = -2;
this.<>t__builder.SetException(ex);
return;
}
this.<>1__state = -2;
this.<>t__builder.SetResult(sum51);
}

[DebuggerHidden]
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
}
}

上面的代码是通过dotPeek(NetReflector也可以是付费软件,使用结束后就用不了了)才反编译出来的,之前试过ILSpy和DnSpy都得不到d__9类定义相关反编译代码。

通过上面的代码可以看到,通过async定义异步方法后,编译器帮我们生成了继承至IAsyncStateMachine的d__9和d__10类,这个两个类正是实现异步的一个关键抽象(个人理解有点像携程里的迭代器封装逻辑代码调用)。

  • 触发OnBtnAwaitTaskUsingClick()方法调用
  • 构建d__9类实例对象状态机,初始化<>t_builder()(AsyncVoidMethodBuilder类型–异步任务构建器,用于保存异步任务结果相关),这里因为OnBtnAwaitTaskUsingClick是void返回类型,所以构建的是AsyncVoieMethodBuilder类对象。初始化状态机<>1__state值为-1
  • 调用stateMachine.<>t__builder.Start<GameLauncher.d__9>(ref stateMachine)触发状态机的MoveNext()的代码执行。
  • 因为<>1__state状态值为-1,所以执行构建5__1(Task类型)的异步AwaitTaskAction执行(默认Task.Run走线程池,所以会运行在不同线程)
  • 这一步AwaitTaskAction异步代码就开始执行了,首先构建d__10类实例对象状态机,初始化<>t__builder(AsyncTaskMethodBuilder类型–异步任务构造器,用于保存异步任务结果相关),这里是因为AwaitTaskAction是Task返回类型,所以构建的是AsyncTaskMethodBuilder类型对象。初始化状态机<>1__state值为-1
  • 调用stateMachine.<>t__builder.Start<GameLauncher.d__10>(ref stateMachine)触发状态机的MoveNext()的代码执行,然后通过d__10.<>t__builder.Task返回AwaitTaskAction的异步任务对象
  • 因为<>1___state状态值为-1,所以首先执行**”AwaitTaskAction Start”**打印,然后获取延迟1秒Task.Delay(1).GetAwaiter()的异步等待完成对象awaiter1
  • 因为Task.Run默认走线程池,所以这里可能回到主线程执行,触发**”AwaitTaskAction has been launched. Main Thread:{Thread.CurrentThread.ManagedThreadId}”**的打印
  • 接着5__1.GetAwaiter()获取AwaitTaskAction的异步等待完成对象(TaskAwaiter类型继承至ICriticalNotifyCompletion),5__1.IsCompleted判定异步没有完成,将<>1__state状态机值设置成0,并保存5__1(AwaitTaskAction的异步任务)到<>u__1
  • 然后调用d__9.<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref statemachine)触发等待AwaitTaskAction等待异步任务完成(异步任务完成后会自动触发状态机的MoveNext()触发状态机继续执行)
  • 主线程d__9状态机在等待AwaitTaskAction异步任务完成,所以Task.Run在另一个线程运行的d__10状态机又得到机会运行
  • d__10.awaiter1.IsCompleted判定异步没有完成,状态机<>1__state值设置为0d__10的异步等待完成对象<>u__1设置成awaiter1(即Task.Delay(1)的异步任务等待完成对象),然后调用<>t__builder.AwaitUnsafeOnCompleted(ref awaiter1, ref statemachine)触发等待一秒等待异步任务完成(异步任务完成后会自动触发状态机的MoveNext()触发状态机继续执行)
  • 等待一秒过后,Task.Delay(1)异步任务完成,触发__d__10状态机的MoveNext执行,此时状态机<>1__state值为0,将Task.Delay(1).GetAwaiter()对象保存到awaiter1,<>u__1构建一个新的TaskAwaiter()对象,<>1__state状态机值设置成-1,然后调用awaiter1.GetResult()结束异步任务完成等待(因为是Task.Delay(1)所以没有结果需要保存),此时打印**”AwaitTaskAction After Task.Delay(1)**
  • 然后开始计算for循环累加的代码,初始化5__1为0,执行5__1的循环累加
  • 累加执行完成后执行Task.Delay(2),此时保存Task.Delay(2).GetAwaiter()的异步等待完成对象到awaiter2,awaiter2.IsCompleted判定异步没有完成,将<>1__state状态机值设置成1,并保存Task.Delay(2).GetAwaiter()到<>u__1,然后调用<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref statemachine)触发等待两秒等待异步任务完成(异步任务完成后会自动触发状态机的MoveNext()触发状态机继续执行)
  • 等待两秒过后,Task.Delay(2)异步任务完成,触发__d__10状态机的MoveNext执行,此时状态机<>1__state值为1,执行awaiter2保存<>u__1(Task.Delay(2).GetAwaiter()的异步任务等待完成对象),同时构建一个新的TaskAwaiter()赋值给<>u__1,并设置**<>1__stat状态机值到-1**
  • 然后直接跳转到代码label_12标签片段,此时Task.Delay(2)异步任务已经完成,调用awaiter.GetResult()直接触发异步任务完成等待
  • 紧接着打印**”AwaitTaskAction After Task.Delay(2)”并将累加结果5__1保存到sum51成员变量,然后将状态机<>1__state值设置成-2**同时将累加结果sum51通过<>t__builder(**AsyncTaskMethodBuilder类型)调用SetResult(sum51)将AwaitTaskAction()方法调用的返回结果设置到异步任务构建器里
  • 上面一套流程下来,AwaitTaskAction的异步代码就算是执行完成了(即d__9状态机里Task.Run(new Func<Task>((object) this.<>4__this, __methodptr(AwaitTaskAction)))的异步方法执行完成了),d__9.<>t__builder.AwaitUnsafeOnCompleted()监听到异步任务完成,触发d__9的MoveNext()执行
  • 此时d__9.<>1__state状态机值为0,执行awaiter保存<>u__1(AwaitTaskAction的异步任务等待完成对象),新创建一个TaskAwaiter异步任务等待完成对象并设置给<>u__1,同时<>1__state状态机值设置成-1
  • 调用awaiter.GetResult()获取AwaitTaskAction异步任务结果并存储到d__9.<>s__3和d__9.5__2
  • 打印**”sumResult:{sumResult}”**,将<>1__state状态机值设置成-2,5__1存储的AwaitTaskAction异步任务置空
  • 最后通过d__9.<>t__builder:SetResult()标记OnBtnAwaitTaskUsingClick的异步任务完成和设置结果(因为OnBtnAwaitTaskUsingClick是void返回所以无需设置结果值)

就这样我们所有的异步线性流程都通过代码生成状态机运行的方式,变成了一个一个的状态按顺序执行。

可以看到异步线性流程和核心由两部分组成:

  1. Task异步运行机制
  2. 异步状态机代码生成

On the C# side of things, the compiler transforms your code into a state machine that keeps track of things like yielding execution when an await is reached and resuming execution when a background job has finished.

Task异步取消

Task异步的取消是通过Cancellation Token来完成的。

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
/// <summary>
/// 响应开启等待取消Task按钮点击
/// </summary>
private async void OnBtnWaitCancelTaskUsingClick()
{
mTaskCancelTokenSource = new CancellationTokenSource();
var taskToken = mTaskCancelTokenSource.Token;
var awaitTask = Task.Run(() =>
{
return AwaitCancelTaskAction(taskToken);
}, taskToken);
Debug.Log($"AwaitCancelTaskAction has been launched. Main Thread:{Thread.CurrentThread.ManagedThreadId}");
try
{
var sumResult = await awaitTask;
Debug.Log($"AwaitCancelTaskAction sumResult:{sumResult}");
Debug.Log($"AwaitCancelTaskAction DateTime.Now.Millisecond:{DateTime.Now.Millisecond}");
}
catch (OperationCanceledException e)
{
Debug.Log(e);
Debug.Log($"e.CancelLlationToken.Equals(taskCancelToken):{e.CancellationToken.Equals(taskToken}");
}
finally
{
mTaskCancelTokenSource.Dispose();
}
}

/// <summary>
/// 等待取消的Task方法
/// </summary>
/// <param name="taskToken"></param>
/// <returns></returns>
private async Task<int> AwaitCancelTaskAction(CancellationToken taskToken)
{
// 检测异步Task是否已经取消
taskToken.ThrowIfCancellationRequested();
Debug.Log($"AwaitCancelTaskAction Start");
Debug.Log($"DateTime.Now.Millisecond:{DateTime.Now.Millisecond} Thread Id:{Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000);
// 检测异步Task是否已经取消
taskToken.ThrowIfCancellationRequested();
Debug.Log($"AwaitCancelTaskAction After Task.Delay(1)");
Debug.Log($"DateTime.Now.Millisecond:{DateTime.Now.Millisecond}");
int sum = 0;
for(int i = 0; i < 10000000; i++)
{
sum++;
}
await Task.Delay(2000);
// 检测异步Task是否已经取消
taskToken.ThrowIfCancellationRequested();
Debug.Log($"AwaitCancelTaskAction After Task.Delay(2)");
Debug.Log($"DateTime.Now.Millisecond:{DateTime.Now.Millisecond}");
Debug.Log($"AwaitCancelTaskAction End");
return sum;
}

/// <summary>
/// 响应Task取消按钮点击
/// </summary>
private void OnBtnCancelTaskUsingClick()
{
mTaskCancelTokenSource?.Cancel();
Debug.Log($"取消异步Task");
}

上面的代码可以看到,我们在开启一个需要支持取消的异步Task时需要以下步骤:

  1. 申请创建了一个CancellationTokenSource对象,这个对象会包含我们异步Task取消所需的token。
  2. 将CancellationTokenSource.token传递给需要异步执行的Task(多层调用要一直传递下去,确保Task正确异步取消)
  3. 在需要异步打断的Task里判定token是否已经取消(token.ThrowIfCancellationRequested())
  4. try catch异步await,确保释放token source(CancellationTokenSource.Dispose())

在上面的使用中可以看到,Task异步是多线程的,通过Task.Run()我们启用了一个新的线程执行任务,所以打印出的Thread Id和Main Thread Id不一样。

为了确保Task取消后异步逻辑能正确停止,我们在异步逻辑里关键地方都在判定taskToken.ThrowIfCancellationRequested()通知Task进入取消状态。

Task总结

以下总结来源:深入解析C#异步编程:await 关键字背后的实现原理

深入理解 AsyncTaskMethodBuilder

AsyncTaskMethodBuilder 是一个辅助类,用于构建和管理异步方法的任务。它提供了以下方法:

  • Create:创建一个新的 AsyncTaskMethodBuilder 实例。
  • Start:开始执行异步方法,调用状态机的 MoveNext 方法。
  • AwaitUnsafeOnCompleted:注册回调函数,当任务完成时触发回调。
  • SetResult:设置任务的结果。
  • SetException:设置任务的异常。
异步方法的生命周期
  1. 初始化:创建状态机实例,初始化状态和任务构建器。
  2. 开始执行:调用 Start 方法开始执行异步方法。
  3. 执行方法体:在 MoveNext 方法中,根据当前状态执行相应的代码。
  4. 遇到 await:检查任务是否完成,如果未完成则注册回调并暂停方法执行。
  5. 任务完成:回调被触发,重新调用 MoveNext 方法,恢复异步方法的执行。
  6. 方法完成:所有异步操作完成,设置任务的结果或异常。
总结
  1. 异步方法的基本概念asyncawait 关键字用于编写异步代码。
  2. 状态机的生成:编译器为每个异步方法生成一个状态机,包含所有局部变量和状态信息。
  3. MoveNext 方法的执行MoveNext 方法是状态机的核心,负责管理和执行异步操作。
  4. 回调函数的注册和触发:
    • 当遇到 await 关键字时,编译器会生成代码来检查任务是否已经完成。
    • 如果任务未完成,注册回调并暂停方法执行。
    • 当任务完成时,回调函数会被触发,重新调用状态机的 MoveNext 方法,从而恢复异步方法的执行。
  5. AwaitUnsafeOnCompleted 方法的作用:在任务完成时注册一个回调函数,回调函数会在任务完成后被触发,从而恢复异步方法的执行。

使用体验

通过上面的学习使用,这里个人讲讲个人Task的使用心得。

好处:

  1. 通过Task异步我们能将异步逻辑跟同步逻辑一样方便的线性流程编写,不再需要编写回调嵌套(回调地狱)的代码。

不方便处:

  1. Task异步需要定义async关键字,导致所有异步方法都需要加上async关键字(async传染)
  2. Task异步取消时,使用者在异步逻辑里需要关心 taskToken是否已经取消(taskToken.ThrowIfCancellationRequested()),这样代码写起来还是比较麻烦
  3. 开启一个异步Task需要通过Task.Run或者Task.Factory.StartNew方法,这样导致我们嵌套调用异步方法传递token时被迫用上闭包这种方式
  4. Task异步是多线程,需要考虑数据多线程访问问题,对开发者多线程相关知识要求更高

自定义Task

通过上面的学习我们了解了Task和async,await关键词是如何组合起来实现异步等待的。但Task默认相关都是带线程池的,如果我们想实现一个单线程的异步Task(好比UniTask)应该如何实现了?

核心需求如下:

  1. 支持async和await的自定义Task
  2. 类似UniTask的单线程Task
  3. 支持常规的单帧等待,指定时长等待Task
  4. 支持Unity不同Update模式的更新驱动选择
  5. 支持异步任务取消
  6. 支持带返回T类型数据和void返回类型的异步任务

以下实现思路参考来源:

每个人都能写UniTask!!!自定义一个TaskLike并完成一个简单的Delay延时教程

在开始实现自定义Task之前,我们首先先设计好类图,明确各个类的功能。

  1. CustomVoidTask

    自定义void返回类型的Task

  2. CustomAsyncVoidMethodBuilder

    用于生成返回void类型的Task结构体的自定义异步任务构造器(struct),通过在对应Task定义添加[AsyncMethodBuilder(typeof(CustomAsyncVoidMethodBuilder))]标签标记指定异步任务构造器

  3. CustomTaskAwaiter

    抽象自定义void返回类型的异步任务等待器

  4. ICustomTaskSource

    抽象一层任务数据,通过依赖反转实现上层DIY的自定义任务void类型(比如CustomDelayTask)不影响整体自定义Task框架。

  5. **CustomVoidTaskCompletionSource **

    自定义返回void类型的任务完成数据抽象(CustomAsyncVoidMethodBuilder会构建),这个类很重要,是我们包装每一个async函数都要生成的一个类,它用来记录整个async函数的完成情况(注意是整个,这个很重要),完成后的回调,以及异常、取消等实现。

  6. CustomTaskLoopManager

    自定义任务循环管理单例类,负责需要注入Update驱动的统一管理(比如实现CustomTask.Delay()和CustomTask.Yield()等效果)

  7. CustomTaskLoopType

自定义任务循环枚举,用于支持类似Unity里不同Update的定义

  1. CustomTaskStatus

​ 自定义异步任务状态枚举

  1. CancellationTokenSourceCancellationToken

采用原生定义的结构辅助设计异步任务取消,ICustomTaskSource子类通过定义SetCanceled方法实现自定义相关的异步任务取消逻辑,CancellationTokenSource和CancellationToken主要用于解决异步嵌套的一系列义务任务打断判定和回调注入

  1. CustomTask

    自定义T返回类型的Task

  2. CustomAsyncMethodBuilder

    用于生成返回T类型的Task结构体的自定义异步任务构造器(struct),通过在对应Task定义添加[AsyncMethodBuilder(typeof(CustomAsyncMethodBuilder))]标签标记指定异步任务构造器

  3. CustomTaskAwaiter

    抽象自定义T返回类型的异步任务等待器

  4. ICustomTaskSource

    抽象一层任务数据,通过依赖反转实现上层DIY的自定义任务返回T类型不影响整体自定义Task框架。

  5. CustomTaskCompletionSource

    自定义返回T类型的任务完成数据抽象(CustomAsyncMethodBuilder会构建),这个类很重要,是我们包装每一个async函数都要生成的一个类,它用来记录整个async函数的完成情况(注意是整个,这个很重要),完成后的回调,以及异常、取消等实现。

  6. CustomDelayTask

    自定义延迟指定毫秒Task

  7. CustomYieldTask

    自定义延迟指定帧数Task

进阶(UniTask)

了解Task异步的基础使用和设计,接下来让我们看看UniTask是如何实现Unity内更优的异步框架设计的。

官网介绍:

UniTask的特点

  • 为 Unity 提供有效的无GC async/await集成。
  • 基于Struct UniTask<T> 的自定义 AsyncMethodBuilder,实现零GC,使所有Unity的异步操作和协程可以await
  • 基于PlayerLoop的Task( UniTask.YieldUniTask.DelayUniTask.DelayFrame 等)这使得能够替换所有协程操作
  • MonoBehaviour 消息事件和 uGUI 事件为可使用Await/AsyncEnumerable
  • 完全在 Unity 的 PlayerLoop 上运行,因此不使用线程,可在 WebGL、wasm 等平台上运行。
  • 异步 LINQ,具有Channel和 AsyncReactiveProperty
  • 防止内存泄漏的 TaskTracker (Task追踪器)窗口
  • 与Task/ValueTask/IValueTaskSource 的行为高度兼容

上面提到UniTask没有使用多线程,这一点对于Unity单线程开发理念更加适合。

更多学习使用待添加……(TODO)

重点知识

  1. Unity子线程内不可访问游戏对象或者组件以及相关方法,只用于处理数据或者逻辑,任何要与主线程发生关系的地方都必须进行回调和上下文切换。
  2. await和async关键词并不一定触发多线程,是否多线程是与使用的具体方式而定(比如Task.Run)

Reference

Asynchronous programming

Task-based Asynchronous Pattern(TAP)

深入解析C#异步编程:await 关键字背后的实现原理

每个人都能写UniTask!!!自定义一个TaskLike并完成一个简单的Delay延时教程

Github

UniTask

文章目录
  1. 1. 前言
  2. 2. 异步编程
    1. 2.1. Task-based Asynchronous Pattern(TAP)
    2. 2.2. 注意事项
    3. 2.3. Task
      1. 2.3.1. Task基础使用
      2. 2.3.2. Task使用Async和Await实现异步方法定义和等待
      3. 2.3.3. Task异步取消
      4. 2.3.4. Task总结
        1. 2.3.4.1. 深入理解 AsyncTaskMethodBuilder
        2. 2.3.4.2. 异步方法的生命周期
        3. 2.3.4.3. 总结
      5. 2.3.5. 使用体验
    4. 2.4. 自定义Task
    5. 2.5. 进阶(UniTask)
  3. 3. 重点知识
  4. 4. Reference
  5. 5. Github