聊一聊 C#异步 任务连续的三种底层玩法

打印 上一主题 下一主题

主题 862|帖子 862|积分 2586

一:配景

1. 讲故事

最近聊了不少和异步相关的话题,有点疲倦了,今天再写最后一篇作为近期这类话题的一个封笔吧,下篇继续写我认识的 生产故障 系列,忽然密切感油然而生,哈哈,免费给别人看程序故障,是一种积阴德阳善的事情,欲知前世因,此生受者是。欲知来世果,此生做者是。
在任务连续方面,我个人的总结就是三类,分别为:

  • StateMachine
  • ContinueWith
  • Awaiter
话不多说,我们逐个研究下底层是咋玩的?
二:异步任务连续的玩法

1. StateMachine

说到状态机大家再认识不外了,也是 async,await 的底层化身,许多人看到 async await 就想到了IO场景,其实IO场景和状态机是两个独立的东西,状态机是一种设计模式,把这个模式套在IO场景会让代码更加丝滑,仅此而已。为了方便讲述,我们写一个 StateMachine 与 IO场景 无关的一段测试代码。
  1.     internal class Program
  2.     {
  3.         static void Main(string[] args)
  4.         {
  5.             UseAwaitAsync();
  6.             Console.ReadLine();
  7.         }
  8.         static async Task<string> UseAwaitAsync()
  9.         {
  10.             var html = await Task.Run(() =>
  11.             {
  12.                 Thread.Sleep(1000);
  13.                 var response = "<html><h1>博客园</h1></html>";
  14.                 return response;
  15.             });
  16.             Console.WriteLine($"GetStringAsync 的结果:{html}");
  17.             return html;
  18.         }
  19.     }
复制代码
那这段代码在底层是怎样运作的呢?刚才也说到了asyncawait只是迷惑你的一种幻象,我们必须手握辟邪宝剑斩开幻象显真身,这里借助 ilspy 截图如下:

从卦中看,本质上就是借助AsyncTaskMethodBuilder 建造者将 awaiter 和 stateMachine 做了一个绑定,感兴趣的朋侪可以追一下 AwaitUnsafeOnCompleted() 方法,最后状态机 d__1 实例会放入到 Task.Run 的 m_continuationObject 字段。如果有朋侪对流程比较蒙的话,我画了一张简图。

图和代码都有了,接下来就是眼见为实。分别在 AddTaskContinuation 和 RunContinuations 方法中做好埋点,前者可以看到 连续任务 是怎么加进去的,后者可以看到 连续任务 是怎么取出来的。


心细的朋侪会发现这卦上有一个很特别的地方,就是 allowInlining=true,也就是回调函数(StateMachine)是在当前线程上一撸到底的。
有些朋侪可能要问,能不能让连续任务 跑在单独线程上? 可以是可以,但你得把 Task.Run 改成 Task.Factory.StartNew ,这样就可以设置TaskCreationOptions参数,参考代码如下:
  1.     var html = await Task.Factory.StartNew(() =>{}, TaskCreationOptions.RunContinuationsAsynchronously);
复制代码
2. ContinueWith

那些同处于被裁的35岁大龄程序员应该知道Task是 framework 4.0 时代出来的,而async,await是4.5出来的,所以在这个过渡期中有大量的项目会使用ContinueWith 导致回调地狱。。。 这里我们对比一下两者有何不同,先写一段参考代码。
  1.     internal class Program
  2.     {
  3.         static void Main(string[] args)
  4.         {
  5.             UseContinueWith();
  6.             Console.ReadLine();
  7.         }
  8.         static Task<string> UseContinueWith()
  9.         {
  10.             var query = Task.Run(() =>
  11.             {
  12.                 Thread.Sleep(1000);
  13.                 var response = "<html><h1>博客园</h1></html>";
  14.                 return response;
  15.             }).ContinueWith(t =>
  16.             {
  17.                 var html = t.Result;
  18.                 Console.WriteLine($"GetStringAsync 的结果:{html}");
  19.                 return html;
  20.             });
  21.             return query;
  22.         }
  23.     }
复制代码
从卦代码看确实没有asyncawait简洁,那 ContinueWith 内部做了什么呢?感兴趣的朋侪可以跟踪一下,本质上和 StateMachine 的玩法是一样的,都是借助 m_continuationObject 来实现连续,画个简图如下:

代码和模型图都有了,接下来就是用 dnspy 开干了。。。还是在 AddTaskContinuation 和 RunContinuations 上埋伏断点观察。


从卦中可以看到,连续任务使用新线程来执行的,并没有一撸到底,这明显与 asyncawait 的方式不同,有些朋侪可能又要说了,那怎样实现和StateMachine一样的呢?这就必要在 ContinueWith 中新增 ExecuteSynchronously 同步参数,参考如下:
  1.     var query = Task.Run(() => { }).ContinueWith(t =>
  2.     {
  3.     }, TaskContinuationOptions.ExecuteSynchronously);
复制代码

3. Awaiter

使用Awaiter做任务连续的朋侪可能相对少一点,它更多的是和 StateMachine 打配合,当然单独使用也可以,但没有前两者灵活,它更得当那些不带返回值的任务连续,本质上也是借助 m_continuationObject 字段实现的一套底层玩法,话不多说,上一段代码:
  1.         static Task<string> UseAwaiter()
  2.         {
  3.             var awaiter = Task.Run(() =>
  4.             {
  5.                 Thread.Sleep(1000);
  6.                 var response = "<html><h1>博客园</h1></html>";
  7.                 return response;
  8.             }).GetAwaiter();
  9.             awaiter.OnCompleted(() =>
  10.             {
  11.                 var html = awaiter.GetResult();
  12.                 Console.WriteLine($"UseAwaiter 的结果:{html}");
  13.             });
  14.             return Task.FromResult(string.Empty);
  15.         }
复制代码
前面两种我配了图,这里没有理由不配了,哈哈,模型图如下:

接下来把程序运行起来,观察截图:


从卦中观察,它和StateMachine一样,默认都是 一撸到底 的方式。
三:RunContinuations 观察

这一末节我们单独说一下 RunContinuations 方法,因为这里的实现太精妙了,不幸的是Dnspy和ILSpy反编译出来的代码太狗血,原汁原味的简化后代码如下:
  1.     private void RunContinuations(object continuationObject) // separated out of FinishContinuations to enable it to be inlined
  2.     {
  3.         bool canInlineContinuations =
  4.             (m_stateFlags & (int)TaskCreationOptions.RunContinuationsAsynchronously) == 0 &&
  5.             RuntimeHelpers.TryEnsureSufficientExecutionStack();
  6.         switch (continuationObject)
  7.         {
  8.             // Handle the single IAsyncStateMachineBox case.  This could be handled as part of the ITaskCompletionAction
  9.             // but we want to ensure that inlining is properly handled in the face of schedulers, so its behavior
  10.             // needs to be customized ala raw Actions.  This is also the most important case, as it represents the
  11.             // most common form of continuation, so we check it first.
  12.             case IAsyncStateMachineBox stateMachineBox:
  13.                 AwaitTaskContinuation.RunOrScheduleAction(stateMachineBox, canInlineContinuations);
  14.                 LogFinishCompletionNotification();
  15.                 return;
  16.             // Handle the single Action case.
  17.             case Action action:
  18.                 AwaitTaskContinuation.RunOrScheduleAction(action, canInlineContinuations);
  19.                 LogFinishCompletionNotification();
  20.                 return;
  21.             // Handle the single TaskContinuation case.
  22.             case TaskContinuation tc:
  23.                 tc.Run(this, canInlineContinuations);
  24.                 LogFinishCompletionNotification();
  25.                 return;
  26.             // Handle the single ITaskCompletionAction case.
  27.             case ITaskCompletionAction completionAction:
  28.                 RunOrQueueCompletionAction(completionAction, canInlineContinuations);
  29.                 LogFinishCompletionNotification();
  30.                 return;
  31.         }
  32.     }
复制代码
卦中的 case 挺有意思的,除了本篇聊过的 TaskContinuation 和 IAsyncStateMachineBox 之外,另有别的两种 continuationObject,这里说一下 ITaskCompletionAction 是怎么回事,其实它是 Task.Result 的底层连续范例,所以大家应该能明白为什么 Task.Result 能唤醒,主要是得益于Task.m_continuationObject =completionAction 所致。
说了这么说,怎样眼见为实呢?可以从源码中寻找答案。
  1.         private bool SpinThenBlockingWait(int millisecondsTimeout, CancellationToken cancellationToken)
  2.         {
  3.             var mres = new SetOnInvokeMres();
  4.             AddCompletionAction(mres, addBeforeOthers: true);
  5.             var returnValue = mres.Wait(Timeout.Infinite, cancellationToken);
  6.         }
  7.         private sealed class SetOnInvokeMres : ManualResetEventSlim, ITaskCompletionAction
  8.         {
  9.             internal SetOnInvokeMres() : base(false, 0) { }
  10.             public void Invoke(Task completingTask) { Set(); }
  11.             public bool InvokeMayRunArbitraryCode => false;
  12.         }
复制代码
从卦中可以看到,其实就是把 ITaskCompletionAction 接口的实现类 SetOnInvokeMres 塞入了 Task.m_continuationObject 中,一旦Task执行完毕之后就会调用 Invoke() 下的 Set() 来实现事件唤醒。
四:总结

虽然异步任务连续有三种实现方法,但底层都是一个套路,即借助 Task.m_continuationObject 字段玩出的各种花样,当然他们也是有一些区别的,即对 m_continuationObject 任务是否用单独的线程调度,产生了不同的意见分歧。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

天津储鑫盛钢材现货供应商

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表