文章目錄
  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. 使用体验
    4. 2.4. 进阶(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
/// <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(1);
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++;
}
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
[AsyncStateMachine((Type) typeof(<AwaitTaskAction>d__10)), DebuggerStepThrough]
private Task<int> AwaitTaskAction()
{
<AwaitTaskAction>d__10 stateMachine = new <AwaitTaskAction>d__10 {
<>4__this = this,
<>t__builder = AsyncTaskMethodBuilder<int>.Create(),
<>1__state = -1
};
stateMachine.<>t__builder.Start<<AwaitTaskAction>d__10>(ref stateMachine);
return stateMachine.<>t__builder.get_Task();
}

[CompilerGenerated]
private sealed class <AwaitTaskAction>d__10 : IAsyncStateMachine
{
// Fields
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;

// Methods
private void MoveNext()
{
int num = this.<>1__state;
try
{
TaskAwaiter awaiter;
GameLauncher.<AwaitTaskAction>d__10 d__;
TaskAwaiter awaiter2;
if (num == 0)
{
awaiter = this.<>u__1;
this.<>u__1 = new TaskAwaiter();
this.<>1__state = num = -1;
}
else if (num == 1)
{
awaiter2 = this.<>u__1;
this.<>u__1 = new TaskAwaiter();
this.<>1__state = num = -1;
goto TR_0005;
}
else
{
// 开始第一个Task并获得awaiter,通过awaiter来观察Task是否完成。
Debug.Log("AwaitTaskAction Start");
Debug.Log($"DateTime.Now.Millisecond:{(int) DateTime.get_Now().Millisecond}");
awaiter = Task.Delay(1).GetAwaiter();
if (!awaiter.IsCompleted)
{
// 向未完成的Task中注册continuation action;
// continuation action会在Task完成时执行;
// 等同于awaiter1.onCompleted(() => this.MoveNext())
this.<>1__state = num = 0;
this.<>u__1 = awaiter;
d__ = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, GameLauncher.<AwaitTaskAction>d__10>(ref awaiter, ref d__);
// return(即交出控制权给AwaitTaskAction的调用者)
return;
}
}
// 第一个Task完成(即Task.Delay(1)那部分代码),获取结果
awaiter.GetResult();
Debug.Log("AwaitTaskAction After Task.Delay(1)");
Debug.Log($"DateTime.Now.Millisecond:{(int) DateTime.get_Now().Millisecond}");
this.<sum>5__1 = 0;
this.<i>5__2 = 0;
while (true)
{
if (this.<i>5__2 < 0x3b9a_ca00)
{
int num3 = this.<sum>5__1;
this.<sum>5__1 = num3 + 1;
num3 = this.<i>5__2;
this.<i>5__2 = num3 + 1;
continue;
}
awaiter2 = Task.Delay(2).GetAwaiter();
if (awaiter2.IsCompleted)
{
goto TR_0005;
}
else
{
this.<>1__state = num = 1;
this.<>u__1 = awaiter2;
d__ = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, GameLauncher.<AwaitTaskAction>d__10>(ref awaiter2, ref d__);
}
break;
}
return;
TR_0005:
// 第二个Task完成(即Task.Delay(2)那部分代码),获取结果
awaiter2.GetResult();
Debug.Log("AwaitTaskAction After Task.Delay(2)");
Debug.Log($"DateTime.Now.Millisecond:{(int) DateTime.get_Now().Millisecond}");
Debug.Log("AwaitTaskAction End");
Debug.Log($"DateTime.Now.Millisecond:{(int) DateTime.get_Now().Millisecond}");
int result = this.<sum>5__1;
this.<>1__state = -2;
this.<>t__builder.SetResult(result);
}
catch (Exception exception)
{
this.<>1__state = -2;
this.<>t__builder.SetException(exception);
}
}

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

上面的代码是通过NetReflector才反编译出来的,之前试过ILSpy和DnSpy都得不到d__9类定义相关反编译代码。

上面的代码只截取了跟AwaitTaskAction这个异步方法相关的部分。通过上面的代码可以看到,通过async定义异步方法后,编译器帮我们生成了继承至IAsyncStateMachine 的AwaitTaskAction>d10类,这个类正是实现异步的一个关键抽象(个人理解有点像携程里的迭代器封装逻辑代码调用)。通过构建一个AwaitTaskAction>d10实例类并调用stateMachine.<>tbuilder.Start<d10>(ref stateMachine)触发了状态机的MoveNext()的代码执行。

通过代码生成,我们定义的异步线性流程被划分成了状态机里的几个状态(1__state),首先状态从-1进行初始化:

1
2
3
4
5
<AwaitTaskAction>d__10 stateMachine = new <AwaitTaskAction>d__10 {
<>4__this = this,
<>t__builder = AsyncTaskMethodBuilder<int>.Create(),
<>1__state = -1
};

触发状态机开始后,遇到第一个Awaiter(await Task.Delay(1))进行异步Task等待完成,同时将状态机切换到状态0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
else
{
// 开始第一个Task并获得awaiter,通过awaiter来观察Task是否完成。
Debug.Log("AwaitTaskAction Start");
Debug.Log($"DateTime.Now.Millisecond:{(int) DateTime.get_Now().Millisecond}");
awaiter = Task.Delay(1).GetAwaiter();
if (!awaiter.IsCompleted)
{
// 向未完成的Task中注册continuation action;
// continuation action会在Task完成时执行;
// 等同于awaiter1.onCompleted(() => this.MoveNext())
this.<>1__state = num = 0;
this.<>u__1 = awaiter;
d__ = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, GameLauncher.<AwaitTaskAction>d__10>(ref awaiter, ref d__);
// return(即交出控制权给AwaitTaskAction的调用者)
return;
}
}

第一个Awaiter等待完成后,状态机继续运行直到遇到第二个Awaiter(await Task.Delay(2))并将状态机状态切换到1:

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
if (num == 0)
{
awaiter = this.<>u__1;
this.<>u__1 = new TaskAwaiter();
this.<>1__state = num = -1;
}
awaiter.GetResult();
Debug.Log("AwaitTaskAction After Task.Delay(1)");
Debug.Log($"DateTime.Now.Millisecond:{(int) DateTime.get_Now().Millisecond}");
this.<sum>5__1 = 0;
this.<i>5__2 = 0;
while (true)
{
if (this.<i>5__2 < 0x3b9a_ca00)
{
int num3 = this.<sum>5__1;
this.<sum>5__1 = num3 + 1;
num3 = this.<i>5__2;
this.<i>5__2 = num3 + 1;
continue;
}
awaiter2 = Task.Delay(2).GetAwaiter();
if (awaiter2.IsCompleted)
{
goto TR_0005;
}
else
{
this.<>1__state = num = 1;
this.<>u__1 = awaiter2;
d__ = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, GameLauncher.<AwaitTaskAction>d__10>(ref awaiter2, ref d__);
}
break;
}
return;

等待第二个Awaiter完成后,继续状态机状态1逻辑执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    else if (num == 1)
{

awaiter2 = this.<>u__1;
this.<>u__1 = new TaskAwaiter();
this.<>1__state = num = -1;
goto TR_0005;
}
TR_0005:
// 第二个Task完成(即Task.Delay(2)那部分代码),获取结果
awaiter2.GetResult();
Debug.Log("AwaitTaskAction After Task.Delay(2)");
Debug.Log($"DateTime.Now.Millisecond:{(int) DateTime.get_Now().Millisecond}");
Debug.Log("AwaitTaskAction End");
Debug.Log($"DateTime.Now.Millisecond:{(int) DateTime.get_Now().Millisecond}");
int result = this.<sum>5__1;
this.<>1__state = -2;
this.<>t__builder.SetResult(result);

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

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

  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(1);
// 检测异步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(2);
// 检测异步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的使用心得。

好处:

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

不方便处:

  1. Task异步需要定义async关键字,导致所有异步方法都需要加上async关键字(async传染)
  2. Task异步取消时,使用者在异步逻辑里需要关心 taskToken是否已经取消(taskToken.ThrowIfCancellationRequested()),这样代码写起来还是比较麻烦
  3. 开启一个异步Task需要通过Task.Run或者Task.Factory.StartNew方法,这样导致我们嵌套调用异步方法传递token时被迫用上闭包这种方式
  4. 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)

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. 使用体验
    4. 2.4. 进阶(UniTask)
  3. 3. 重点知识
  4. 4. Reference
  5. 5. Github