C# async await 平时用的不多,每次用都要查一遍资料,看完就忘,特此记录一下。
以下为
用例
void ClickMethod()
{
Log($"开始测试 ThreadId:{System.Threading.Thread.CurrentThread.ManagedThreadId}");
TestAsync();
Log($"TestAsync 主线程完成 ThreadId:{System.Threading.Thread.CurrentThread.ManagedThreadId}");
}
//使用async/await异步方法
static async System.Threading.Tasks.Task TestAsync()
{
var async = MyTaskAsync();
Log($"TestAsync await之前 ThreadId:{System.Threading.Thread.CurrentThread.ManagedThreadId}");
await async;
Log($"TestAsync await之后 ThreadId:{System.Threading.Thread.CurrentThread.ManagedThreadId}");
}
//真正开线程的方法
static System.Threading.Tasks.Task<string> MyTaskAsync()
{
return System.Threading.Tasks.Task.Run(() =>
{
Log($"Task.Run MyTaskAsync 线程开始 ThreadId:{System.Threading.Thread.CurrentThread.ManagedThreadId}");
System.Threading.Thread.Sleep(1000);
Log($"Task.Run MyTaskAsync sleep 1秒 线程结束 ThreadId:{System.Threading.Thread.CurrentThread.ManagedThreadId}");
return "task";
});
}
//输出方法
static void Log(string msg)
{
UnityEngine.Debug.Log(msg);
}
TestAsync就是使用C#的async/await语法的异步方法
MyTaskAsync是真正开线程的方法,在里面Sleep了1秒
await之后的代码会等待MyTaskAsync的线程执行完毕,才会执行
Unity进行测试
把这段代码复制到Unity的脚本中执行。
第一步:我们执行了TestAsync,此时打开了一个线程,线程id是27
第二步:线程27开始Sleep,主线程1执行到await,离开了TestAsync方法 输出:主线程完成
第三步:1秒后,线程27执行结束,await等待也结束,执行await之后的代码 线程id是1
在await之前的所有输出,我们就把await当做是return, 写过多线程的话,就能理解。Task.Run的lamda表达式里的都是在子线程3执行,主线程1不受影响,从await(return)出来之后继续执行。
异步方法做了什么?
反编译查看c#代码
先看一下dnspy C#是什么样的
首先Output是我这些方法所在的类名,所以可以忽略。
<>c,只要这个类里有lamda表达式,编译器就会有这个类,<>9是这个<>c的单例(实例)
<MyTaskAsync>b__5_0就是我们写在Taski.Run Lamda的代码啦,一模一样的,相当于编译器帮我们从表达式变成了方法。这个不是重点,就这样吧。
来看TestAsync,可以说跟我们写的代码,不能说是一模一样,简直是毫不相干。
编译器生成了一个类型<TestAsync>d__6,是结构体(编译Release是结构体,Debug是类,算是代码优化),这里就直接是局部变量d__;
d__.<>t__builder = AsyncTaskMethodBuilder.Create();
AsyncTaskMethodBuilder.Create() 建造者模式吧,创建了一个AsyncTaskMethodBuilder赋值给
d__.<>1__state = -1;
state赋值了-1
然后调用了上面builder的Start方法,并返回的builder里的Task
<TestAsync>d__6
[StructLayout(LayoutKind.Auto)]
private struct <TestAsync>d__6 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
private TaskAwaiter<string> <>u__1;
private void MoveNext()
{
int num = <>1__state;//前面有提到,一开始就是-1
try
{
TaskAwaiter<string> awaiter;
//前面有提到,一开始就是-1
if (num != 0)
{
Task<string> task = MyTaskAsync();
Log($"TestAsync await之前 ThreadId:{Thread.CurrentThread.ManagedThreadId}");
awaiter = task.GetAwaiter();
if (!awaiter.IsCompleted)
{
//这里state变为0
num = (<>1__state = 0);
<>u__1 = awaiter;
//这个内部做了很多操作,大概就是等线程做完了,会再回调到MoveNext
//再次回调到MoveNext时,走的是下面else逻辑
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter<string>);
num = (<>1__state = -1);
}
awaiter.GetResult();
Log($"TestAsync await之后 ThreadId:{Thread.CurrentThread.ManagedThreadId}");
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
<>t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
IAsyncStateMachine(微软文档:表示为异步方法生成的状态机。此类型仅供编译器使用。)
MoveNext(微软文档:将状态机移动到其下一个状态。)
public void MoveNext ();
SetMachineState(微软文档:使用堆分配的副本配置状态机。)
public void SetStateMachine (System.Runtime.CompilerServices.IAsyncStateMachine stateMachine);
await后的代码为什么在主线程执行?
await,等待子线程3执行完,为什么还是在主线程1呢?我们在前面可以看到,早在线程3执行完之前,主线程1就已经从TestAsync方法出来,做别的事了。怎么又回到线程1的?
同样的用例,放在Unity和WPF执行,输出是一样的,await之后的代码都是主线程。
然而在.net core执行则不同,await之后的代码线程id与Task.Run是一样的(我认为这更符合平时多线程编程的理解)。
这大概说明,不同平台实现方式不同。unity和WPF有图形界面,可能在状态机切换过程中回归了主线程。