给之前工作上调研的async/await异步用法,做个笔记。
1. 规则
a. async/await两个关键字成对出现、配合使用。
b. async用于修饰方法,放在方法类型前面。
c. await 用于方法体中,后面跟一个可等待对象awaitable。
d. 返回类型为Task<T>、Task或void。
2. async/await执行机制
a. 在async方法中,遇到await关键字,若awaitable已经完成,则方法会继续执行;若awaitable尚未完成,则awaitable会异步执行,并从该处返回。
b. 当awaitable完成后,会在“await”返回前捕获的“上下文”上执行后续代码。
3. 返回类型规则
a. Task<T>:代表一个返回值为T类型的操作。函数必须返回T类型的值。调用该aync方法需使用await。
private async Task<int> CalculateAnswer()
{
await Task.Delay(100);
//必须返回int类型
return 10;
}
//调用CalculateAnswer
async void Test()
{
await CalculateAnswer();
}
b. Task:代表一个无返回值的操作。调用该aync方法需使用await。
private async Task DoSomethingAsync()
{
//Task.Delay的返回值是Task
await Task.Delay(3000);
}
//调用DoSomethingAsync
async void Test()
{
await DoSomethingAsync();
}
c. void:主要用于事件处理程序Event Handler。可直接调用该aync方法。
//下载按钮的点击事件中,向服务器发送请求,等待返回结果后做某些处理
downloadButton.onClick.AddListener(async () =>
{
var ret = await LobbyService.Instance.ClientCreateLobbyAsync();
Debug.Log(ret.Error);
if (ret.Result != null && ret.Result.Code == 0)
{
Debug.Log(ret.Result);
}
});
void Start()
{
//直接调用,不能使用await
Test();
}
async void Test()
{
await DoSomethingAsync();
}
4. 注意事项
a. 若awaitable完成时,脚本所在的对象或脚本本身已被删除,后续代码仍会执行。
规则:对对象进行非空判断
private async Task DoSomethingAsync()
{
print("Task Start");
await Task.Delay(3000);
print("Task End");
if(null == this)
{
return;
}
m_iValue = 2;//对成员变量赋值
print($"m_iValue:{m_iValue}");
FunctionTest();//调用成员函数
}
b. Unity游戏对象只能在主线程中访问和操作。
i. 建议:本身使用async void方法定义,或者使用async void方法来调用
private async void DoSomethingAsync()
{
Transform cache = GetComponent();
await Task.Delay(1000);
transform.position = Vector3.one;
print(gameObject.name);
}
async void Start()
{
await DoSomethingAsync();
}
private async Task DoSomethingAsync()
{
Transform cache = GetComponent();
await Task.Delay(1000);
transform.position = Vector3.one;
print(gameObject.name);
}
ii. 建议:引入Loom插件脚本(参见参考文档),将gameObject相关操作通过Loom.QueueOnMainThread函数在主线程执行。
//利用Loom.QueueOnMainThread封装一个判断gameObject是否为空的函数
private Task IsGameObjectAvailable()
{
TaskCompletionSource tcs = new TaskCompletionSource();
Loom.QueueOnMainThread(() =>
{
//设置任务结果
tcs.SetResult(null != this && null != gameObject);
});
return tcs.Task;
}
private async Task DoSomethingAsynsWithLoom()
{
await Task.Delay(2000);
if (!await IsGameObjectAvailable())
{
return;
}
}
iii. 建议:将gameObject作为参数传入。
private async Task DoSomethingAsyncWithParam(GameObject myGameObject)
{
await Task.Delay(2000);
if (null == myGameObject)
{
return;
}
}
c. 存在有的异常不会报错但会直接中断执行的情况。
建议:可根据具体情况考虑采用try catch捕获异常。需要注意的是async void方法只能在方法内捕获异常。
void Start()
{
//无法捕获,会在22行直接报错
try
{
Test();
}
catch (Exception e)
{
print($"Exception: {e.Message}");
}
Test2();
}
private async void Test()
{
Transform cache = null;
await Task.Delay(1000);
cache.position = Vector3.one;
}
private async void Test2()
{
Transform cache = null;
await Task.Delay(1000);
//可以捕获空异常
try
{
cache.position = Vector3.one;
}
catch (Exception e)
{
print($"Exception: {e.Message}");
}
}
d. 上下文的使用:默认情况下未完成的awaitable将捕获当前上下文以用于其完成时执行后续代码。它可以是UI上下文或者ASP.NET请求上下文,若两者为空,则是线程池上下文。
i. 建议:避免死锁和性能问题,在不需要上下文的情况下尽量使用ConfigureAwait(false)来丢弃上下文
async Task Test()
{
await Task.Delay(1000).ConfigureAwait(false);
}
ii. 建议:每个async方法都有自己的上下文。无上下文的代码更具重用性。在设计函数时考虑将上下文敏感的代码和上下文无关的代码进行分隔。
void Start()
{
downloadButton.onClick.AddListener(async () =>
{
downloadButton.enabled = false;
//后续代码需要当前上下文,不使用ConfigureAwait(false)
await HandleClickAsync();
downloadButton.enabled = true;
});
}
//处理上下文无关的异步功能
private async Task HandleClickAsync()
{
await Task.Delay(1000).ConfigureAwait(false);
}
5. 参考文档
a. C# 异步和等待,async/await
b. C#Task的创建和Wait等
c. 韦_恩带你用好async/await异步多线程(C#5.0引入的特性)
d. Loom工具
e. Async/Await - Best Practices in Asynchronous Programming
本文详细介绍了C#中的async/await关键字用法,包括规则、执行机制、返回类型、注意事项以及在Unity中的应用。同时提供了如何处理异常、上下文管理和最佳实践的建议。
570






