task async await后续
几个月前写过一篇关于task、async和await的帖子,给我感觉还是比较抽象复杂的,涉及到了线程、il代码、编译器、关键字等等,有兴趣的朋友可以转过去看看,。有了上一篇task的基础,那么今天我这边就来简单聊一下ConfigureAwait背后的原理,首先声明,只是个人读源码的一些理解,理解不对的,看官请指出,以下所有源码都是基于.NET6.0。
Task,在聊ConfigureAwait之前,我们先简单回顾下task、async、await。task依然还是那个task,很复杂的一个对象,表示异步操作,默认情况下以线程池的方式执行,也可以以线程的方式执行,看代码
protected internal override void QueueTask(Task task)
{
TaskCreationOptions options = task.Options;
if (Thread.IsThreadStartSupported && (options & TaskCreationOptions.LongRunning) != 0)
{
new Thread(s_longRunningThreadWork)
{
IsBackground = true,
Name = ".NET Long Running Task"
}.UnsafeStart(task);
}
else
{
ThreadPool.UnsafeQueueUserWorkItemInternal(task, (options & TaskCreationOptions.PreferFairness) == 0);
}
}
如上代码,最终task调用TaskScheduler调度器的QueueTask方法完成任务的调度,创建线程或者以线程池的方式执行,task有3种调度器,这只是其中一种。
Async,方法修饰符,关键字,表示该方法可能会有异步操作,与普通方法别无二致,async关键字主要是提供给编译器编译用的,看代码
private static async Task Test()
{
Console.WriteLine(2);
var v1 = await b();
var v = await a();
Console.WriteLine(6);
}
private static async Task b()
{
Thread.Sleep(2000);
return "3";
}
private static async Task a()
{
Console.WriteLine(3);
await Task.Run(() =>
{
Thread.Sleep(10000);
Console.WriteLine(4);
});
Console.WriteLine(5);
return "3";
}
static void Main(string[] args)
{
Console.WriteLine(1);
var v = Test();
Console.WriteLine(7);
Console.Read();
}
以上就是我的测试代码,随便写的,此时我们通过vs编译器编译生成,看看编译器对它干了啥,看图
我们的async方法被编译成了一个个的类class,不过这些类class有些特别,继承了IAsyncStateMachine接口,同时提供了三个方法,构造器、movenext、setstatemachine,后面这两方法是实现了IAsyncStateMachine接口,IAsyncStateMachine就是大家说的状态机,有了状态机,状态何来?接着看。
await,await必须要在async修饰的方法里面声明,这也就很好理解了,async提供状态机,await提供状态,我是这么理解的,不知道对不对哈。await同样是关键字,提供给编译器编译用的,接着以上代码,我们简单看下编译器针对await关键字的编译行为,看代码
有启动线程的il代码(a_class)
IL_0054: dup
IL_0055: stsfld class [System.Runtime]System.Action ConsoleApp5.Program/'<>c'::'<>9__4_0'
IL_005a: call class [System.Runtime]System.Threading.Tasks.Task [System.Runtime]System.Threading.Tasks.Task::Run(class [System.Runtime]System.Action)
IL_005f: callvirt instance valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter [System.Runtime]System.Threading.Tasks.Task::GetAwaiter()
IL_0064: stloc.3
IL_0065: ldloca.s V_3
IL_0067: call instance bool [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted()
IL_006c: brtrue.s IL_00af // 如果为true也就是任务跑完了,直接跳转指令到获取返回值,那部分代码我没贴了
IL_006e: ldarg.0
IL_006f: ldc.i4.0
IL_0070: dup
IL_0071: stloc.0
IL_0072: stfld int32 ConsoleApp5.Program/'d__4'::'<>1__state'
IL_0077: ldarg.0
IL_0078: ldloc.3
IL_0079: stfld valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter ConsoleApp5.Program/'d__4'::'<>u__1'
IL_007e: ldarg.0
IL_007f: stloc.s V_4
IL_0081: ldarg.0
IL_0082: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 ConsoleApp5.Program/'d__4'::'<>t__builder'
IL_0087: ldloca.s V_3
IL_0089: ldloca.s V_4
IL_008b: call instance void valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1::AwaitUnsafeOnCompletedd__4'>(!!0&, // 开始跑任务状态
未启动线程的il代码(b_class)
IL_0025: call instance string [System.Runtime]System.Int32::ToString()
IL_002a: call string [System.Runtime]System.String::Concat(string,
string)
IL_002f: call void [System.Console]System.Console::WriteLine(string)
IL_0034: nop
IL_0035: ldstr "3"
IL_003a: stloc.1
IL_003b: leave.s IL_0055 // 执行方法体里面的代码,直接执行leave指令,跳转到指定地址
test方法的调用栈
IL_00aa: stfld string ConsoleApp5.Program/'d__0'::'<>s__6'
IL_00af: call class [System.Runtime]System.Threading.Tasks.Task`1 ConsoleApp5.Program::b() // 创建b对象
IL_00b4: callvirt instance valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1 class [System.Runtime]System.Threading.Tasks.Task`1::GetAwaiter()
IL_00b9: stloc.3
IL_00ba: ldloca.s V_3
IL_00bc: call instance bool valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1::get_IsCompleted() // 判断任务是否完成,这里是true
IL_00c1: brtrue.s IL_0106 // 直接跳转获取result
以上代码我贴了三个class的部分il代码,a、b和test,也做了简单注释,a类里面声明了await关键字,并且开启了新的线程,b类里面没有声明await关键字,大家可以简单看下il代码,这里面有几个小地方需要注意,1. await等待对象如果没有开启线程,那么它还是同步方式执行的,如果在没有开启新线程的情况下,你还坚持声明async、await关键字,我只能说,会影响性能。2. await等待对象里面如果没有开启新线程,那么它阻塞是的调用线程,如果有开启线程,那么它阻塞的是“当前”方法调用栈。简单总结一下,以var v1 = await b();这行代码为例,这行代码所在的方法会被编译成class,b方法同样也被编译成了b类class,同时实现了三个方法构造器和movenext,movenext方法体里面的内容就是我们定义的b方法体的内容,调用线程先调用了被编译器编译成class的b的构造器,在b的构造器里面,调用了movenext函数,因为b的方法体里面没有开启新的线程,调用线程会执行完b的方法体,再return出来,获取awaiter对象,接着判断这个movenext函数是否执行完毕,如果执行完毕,直接获取result,调用线程接着执行下面的逻辑,如果b方法里面开启了新的线程,背后逻辑会有区别。task、async、await回顾就到这吧。
ConfigureAwait是什么?其实它就是一个函数啊,说的有点废话,它能干嘛?防止死锁,提高性能,那么我就好奇了,它是怎么做到的,怀着好奇心,我从github上down了源代码,github地址,https://github.com/dotnet/runtime,runtime?没错,.NET基础库都在这里,包括clr。源码一打开,把我惊呆了,这个函数的实现就一行代码,直接return一个ConfiguredTaskAwaitable对象,看代码,
public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext)
{
return new ConfiguredTaskAwaitable(this, continueOnCapturedContext);
}
如上就是ConfigureAwait方法的实现,看到这里,我想朋友们跟我一样也有很多疑惑,玄机应该就在 ConfiguredTaskAwaitable这个对象里面,看下它的定义。
public readonly struct ConfiguredTaskAwaitable
{
private readonly ConfiguredTaskAwaitable.ConfiguredTaskAwaiter m_configuredTaskAwaiter;
public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter()
{
return m_configuredTaskAwaiter;
}
...
}
其他成员代码我就不贴了,这里我们只需要关注的就是getAwaiter方法,以及它的返回值ConfiguredTaskAwaiter,继续看下它的定义。
public readonly struct ConfiguredTaskAwaiter : ICriticalNotifyCompletion, IConfiguredTaskAwaiter
{
internal readonly Task m_task;
internal ConfiguredTaskAwaiter(Task task, bool continueOnCapturedContext)
{
Debug.Assert(task != null, "Constructing an awaiter requires a task to await.");
m_task = task;
m_continueOnCapturedContext = continueOnCapturedContext;
}
public bool IsCompleted => m_task.IsCompleted;
public void OnCompleted(Action continuation)
{
TaskAwaiter.OnCompletedInternal(m_task, continuation, m_continueOnCapturedContext, flowExecutionContext: true);
}
public void UnsafeOnCompleted(Action continuation)
{
TaskAwaiter.OnCompletedInternal(m_task, continuation, m_continueOnCapturedContext, flowExecutionContext: false);
}
public void GetResult()
{
TaskAwaiter.ValidateEnd(m_task);
}
}
以上就是ConfiguredTaskAwaiter的全貌,这里简单理一下awaiter成员函数的作用以及背后调用逻辑(讨论的是编译器行为,因为是await表达式),以代码var vd = await d().ConfigureAwait(false);为例,1. 调用GetAwaiter()方法获取ConfiguredTaskAwaiter对象,2. 随后判断awaiter对象的get_IsCompleted是否为true?(此处如果d方法里面未开启新的线程,这里就会返回true,反之就会返回false),3. 如果为true则调用上面的GetResult()方法,获取结果,如果为false,则调用AwaitUnsafeOnCompleted()方法,Hook通知,待任务完成后调用上面的UnsafeOnCompleted()方法,重新回到状态机movenext,调用GetResult()方法获取结果,4. 接着执行后续逻辑。awaiter不是只有ConfiguredTaskAwaiter对象,还有TaskAwaiter等待,默认情况下如await d();返回的就是TaskAwaiter对象,有兴趣的朋友也可以实现自己的awaiter对象,只要符合以上able和awaiter约定就行,这个就不展开了,要实现一个成熟的库还是比较复杂的。
ConfiguredTaskAwaiter对象的所有方法都介绍了,也介绍了背后调用逻辑,接下来继续探索它能解决死锁问题,其实逻辑已经很清晰了,那就是hook到任务完成之后的执行逻辑,我们需要跟踪的就是AwaitUnsafeOnCompleted()方法,看代码。
internal static void AwaitUnsafeOnCompleted如上代码,我们继续跟踪UnsafeOnCompletedInternal方法,( ref TAwaiter awaiter, IAsyncStateMachineBox box) where TAwaiter : ICriticalNotifyCompletion { if ((null != (object?)default(TAwaiter)) && (awaiter is ITaskAwaiter)) { ref TaskAwaiter ta = ref Unsafe.As (ref awaiter); // relies on TaskAwaiter/TaskAwaiter having the same layout TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, continueOnCapturedContext: true); } else if ((null != (object?)default(TAwaiter)) && (awaiter is IConfiguredTaskAwaiter)) { ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter ta = ref Unsafe.As (ref awaiter); TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, ta.m_continueOnCapturedContext); } ... }
if (continueOnCapturedContext) // 这个就是最开始传入的false,默认为true
{
SynchronizationContext? syncCtx = SynchronizationContext.Current; // 获取SynchronizationContext同步上下文,aspnetcore已经取消了这个对象
if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext))
{
var tc = new SynchronizationContextAwaitTaskContinuation(syncCtx, stateMachineBox.MoveNextAction, flowExecutionContext: false);
if (!AddTaskContinuation(tc, addBeforeOthers: false))
{
tc.Run(this, canInlineContinuationTask: false); // 最终通过c.m_syncContext.Post(s_postCallback, c.m_action); Post callback
}
return;
}
else
{
TaskScheduler? scheduler = TaskScheduler.InternalCurrent;
if (scheduler != null && scheduler != TaskScheduler.Default)
{
var tc = new TaskSchedulerAwaitTaskContinuation(scheduler, stateMachineBox.MoveNextAction, flowExecutionContext: false);
if (!AddTaskContinuation(tc, addBeforeOthers: false))
{
tc.Run(this, canInlineContinuationTask: false);
}
return;
}
}
}
if (!AddTaskContinuation(stateMachineBox, addBeforeOthers: false)) // 我们显示指定false,所以会走这个逻辑。
{
ThreadPool.UnsafeQueueUserWorkItemInternal(stateMachineBox, preferLocal: true);
}
以上代码做了简单的注释,最后我个人的理解就是,在aspnetcore环境下没有死锁的风险,但是在GUI环境下面,需要注意死锁和UI线程上下文环境,好了就到这吧。