首页 > 编程语言 >聊一聊 C#异步 任务延续的三种底层玩法

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

时间:2025-01-09 15:00:19浏览次数:1  
标签:case 异步 Task return continuationObject C# html 聊一聊 var

一:背景

1. 讲故事

最近聊了不少和异步相关的话题,有点疲倦了,今天再写最后一篇作为近期这类话题的一个封笔吧,下篇继续写我熟悉的 生产故障 系列,突然亲切感油然而生,哈哈,免费给别人看程序故障,是一种积阴德阳善的事情,欲知前世因,今生受者是。欲知来世果,今生做者是。

在任务延续方面,我个人的总结就是三类,分别为:

  1. StateMachine
  2. ContinueWith
  3. Awaiter

话不多说,我们逐个研究下底层是咋玩的?

二:异步任务延续的玩法

1. StateMachine

说到状态机大家再熟悉不过了,也是 async,await 的底层化身,很多人看到 async await 就想到了IO场景,其实IO场景和状态机是两个独立的东西,状态机是一种设计模式,把这个模式套在IO场景会让代码更加丝滑,仅此而已。为了方便讲述,我们写一个 StateMachine 与 IO场景 无关的一段测试代码。


    internal class Program
    {
        static void Main(string[] args)
        {
            UseAwaitAsync();

            Console.ReadLine();
        }

        static async Task<string> UseAwaitAsync()
        {
            var html = await Task.Run(() =>
            {
                Thread.Sleep(1000);
                var response = "<html><h1>博客园</h1></html>";
                return response;
            });
            Console.WriteLine($"GetStringAsync 的结果:{html}");
            return html;
        }
    }

那这段代码在底层是如何运作的呢?刚才也说到了asyncawait只是迷惑你的一种幻象,我们必须手握辟邪宝剑斩开幻象显真身,这里借助 ilspy 截图如下:

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

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


心细的朋友会发现这卦上有一个很特别的地方,就是 allowInlining=true,也就是回调函数(StateMachine)是在当前线程上一撸到底的。

有些朋友可能要问,能不能让延续任务 跑在单独线程上? 可以是可以,但你得把 Task.Run 改成 Task.Factory.StartNew ,这样就可以设置TaskCreationOptions参数,参考代码如下:

    var html = await Task.Factory.StartNew(() =>{}, TaskCreationOptions.RunContinuationsAsynchronously);

2. ContinueWith

那些同处于被裁的35岁大龄程序员应该知道Task是 framework 4.0 时代出来的,而async,await是4.5出来的,所以在这个过渡期中有大量的项目会使用ContinueWith 导致回调地狱。。。 这里我们对比一下两者有何不同,先写一段参考代码。


    internal class Program
    {
        static void Main(string[] args)
        {
            UseContinueWith();

            Console.ReadLine();
        }

        static Task<string> UseContinueWith()
        {
            var query = Task.Run(() =>
            {
                Thread.Sleep(1000);
                var response = "<html><h1>博客园</h1></html>";
                return response;
            }).ContinueWith(t =>
            {
                var html = t.Result;
                Console.WriteLine($"GetStringAsync 的结果:{html}");
                return html;
            });

            return query;
        }
    }

从卦代码看确实没有asyncawait简洁,那 ContinueWith 内部做了什么呢?感兴趣的朋友可以跟踪一下,本质上和 StateMachine 的玩法是一样的,都是借助 m_continuationObject 来实现延续,画个简图如下:

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


从卦中可以看到,延续任务使用新线程来执行的,并没有一撸到底,这明显与 asyncawait 的方式不同,有些朋友可能又要说了,那如何实现和StateMachine一样的呢?这就需要在 ContinueWith 中新增 ExecuteSynchronously 同步参数,参考如下:

    var query = Task.Run(() => { }).ContinueWith(t =>
    {
    }, TaskContinuationOptions.ExecuteSynchronously);

3. Awaiter

使用Awaiter做任务延续的朋友可能相对少一点,它更多的是和 StateMachine 打配合,当然单独使用也可以,但没有前两者灵活,它更适合那些不带返回值的任务延续,本质上也是借助 m_continuationObject 字段实现的一套底层玩法,话不多说,上一段代码:


        static Task<string> UseAwaiter()
        {
            var awaiter = Task.Run(() =>
            {
                Thread.Sleep(1000);
                var response = "<html><h1>博客园</h1></html>";
                return response;
            }).GetAwaiter();

            awaiter.OnCompleted(() =>
            {
                var html = awaiter.GetResult();
                Console.WriteLine($"UseAwaiter 的结果:{html}");
            });

            return Task.FromResult(string.Empty);
        }

前面两种我配了图,这里没有理由不配了,哈哈,模型图如下:

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


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

三:RunContinuations 观察

这一小节我们单独说一下 RunContinuations 方法,因为这里的实现太精妙了,不幸的是Dnspy和ILSpy反编译出来的代码太狗血,原汁原味的简化后代码如下:

    private void RunContinuations(object continuationObject) // separated out of FinishContinuations to enable it to be inlined
    {
        bool canInlineContinuations =
            (m_stateFlags & (int)TaskCreationOptions.RunContinuationsAsynchronously) == 0 &&
            RuntimeHelpers.TryEnsureSufficientExecutionStack();

        switch (continuationObject)
        {
            // Handle the single IAsyncStateMachineBox case.  This could be handled as part of the ITaskCompletionAction
            // but we want to ensure that inlining is properly handled in the face of schedulers, so its behavior
            // needs to be customized ala raw Actions.  This is also the most important case, as it represents the
            // most common form of continuation, so we check it first.
            case IAsyncStateMachineBox stateMachineBox:
                AwaitTaskContinuation.RunOrScheduleAction(stateMachineBox, canInlineContinuations);
                LogFinishCompletionNotification();
                return;

            // Handle the single Action case.
            case Action action:
                AwaitTaskContinuation.RunOrScheduleAction(action, canInlineContinuations);
                LogFinishCompletionNotification();
                return;

            // Handle the single TaskContinuation case.
            case TaskContinuation tc:
                tc.Run(this, canInlineContinuations);
                LogFinishCompletionNotification();
                return;

            // Handle the single ITaskCompletionAction case.
            case ITaskCompletionAction completionAction:
                RunOrQueueCompletionAction(completionAction, canInlineContinuations);
                LogFinishCompletionNotification();
                return;
        }
    }

卦中的 case 挺有意思的,除了本篇聊过的 TaskContinuation 和 IAsyncStateMachineBox 之外,还有另外两种 continuationObject,这里说一下 ITaskCompletionAction 是怎么回事,其实它是 Task.Result 的底层延续类型,所以大家应该能理解为什么 Task.Result 能唤醒,主要是得益于Task.m_continuationObject =completionAction 所致。

说了这么说,如何眼见为实呢?可以从源码中寻找答案。


        private bool SpinThenBlockingWait(int millisecondsTimeout, CancellationToken cancellationToken)
        {
            var mres = new SetOnInvokeMres();

            AddCompletionAction(mres, addBeforeOthers: true);

            var returnValue = mres.Wait(Timeout.Infinite, cancellationToken);
        }

        private sealed class SetOnInvokeMres : ManualResetEventSlim, ITaskCompletionAction
        {
            internal SetOnInvokeMres() : base(false, 0) { }
            public void Invoke(Task completingTask) { Set(); }
            public bool InvokeMayRunArbitraryCode => false;
        }

从卦中可以看到,其实就是把 ITaskCompletionAction 接口的实现类 SetOnInvokeMres 塞入了 Task.m_continuationObject 中,一旦Task执行完毕之后就会调用 Invoke() 下的 Set() 来实现事件唤醒。

四:总结

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

标签:case,异步,Task,return,continuationObject,C#,html,聊一聊,var
From: https://www.cnblogs.com/huangxincheng/p/18662162

相关文章

  • CDS标准视图:银行对账单行项目 I_BankStatementItem
    视图名称:银行对账单行项目I_BankStatementItem视图类型:基础视图视图代码:点击查看代码@AbapCatalog.sqlViewName:'IBANKSTATMENTITM'@AbapCatalog.compiler.compareFilter:true@AbapCatalog.preserveKey:true@AccessControl.authorizationCheck:#CHECK@EndUserText.l......
  • 模拟ic入门——设计一个带隙基准Bandgap(三)高阶温度补偿与启动电路设计
    上一节我们介绍了Bandgap相关的参数,以及做了其中一个经典电路的电压模仿真,但如果对于温度系数有较高的要求,可以进行高阶温度补偿,本节我们来介绍高阶温度补偿,以及一些启动电路的设计,会附上一些经典的论文供大家学习一、电流模Bandgap首先我们进行电流模bandgap的仿真,运放我采......
  • C语言基础语法_03
    5、函数    函数就是程序中独立的功能,其实就是将程序打包,取一个名字,方便后面重复使用。函数的使用提高了代码的复用性和可维护性。 /*函数的定义:返回值类型函数名(形参1,形参2……){函数体;return返回值;}*/        首先先定义一个简单的不......
  • 【JavaScript编程】并行运行Promise
    举个例子,如果现在我们想要获取三个国家的基本信息,但是这个顺序是无所谓的;按照我们之前的学习的异步代码,如下:constget3Countries=asyncfunction(c1,c2,c3){try{const[data1]=awaitgetJSON(`https://restcountries.com/v2/name/${c1}`);const[data2]=......
  • AIGC提示词
    AIGC的Prompt(提示词)指的是用户向AI系统提供的简短指令或信息,用于引导AI生成符合期望的内容。以下是对AIGC的Prompt的详细解释:一、定义与作用定义:Prompt在AIGC中通常表现为一段文本输入,它可以是问题、指令、引子或其他形式的文本。作用:设置上下文,让AI模型理解用户的意图,并生成......
  • UART异步串行通信协议
    UART概述UART的定义USART指通用同步收发器,UART指通用异步收发器这些通用收发器提供了一种灵活的方式与外部设备进行单工/半双工/全双工方式的数据交互,并且可选择多种波特率,支持多种通信协议和功能模式等UART的类别STM8S单片机片内总共有3个串口资源:UART1/2/3(STM8S105则只......
  • docker安装jellyfine实现家庭影院
    安装环境为Ubuntuserver20.04。管理员账户登陆系统。新建应用安装目录,用于缓存应用使用过程中生成的中间数据,如配置、预览等,最好建到速度较快的SSD硬盘上,可提高体验。mkdir/mnt/ssd/jellyfin-app/configmkdir/mnt/ssd/jellyfin-app/cache拉取镜像并安装。dockerpu......
  • 1.9 CW 模拟赛 赛时记录
    前言策略不变,继续搞看题\(4\)神秘,开骗\(\rm{T1}\)思路先考虑对于一个确定的\(a\)怎么做发现一个数能否被删除与删除的顺序无关,本质上是因为\(j,l\)并不因为操作而改变考虑到一个数能被删除,仅当其在前后缀中都不为最大值,也就是说可以\(\mathcal{O}(n)\)......
  • .NET Core:架构、特性和优势详解
    .NETCore:架构、特性和优势详解在软件开发领域,保持领先地位至关重要。随着技术以指数级的速度发展,开发人员不断寻求高效、可扩展且多功能的解决方案来应对现代挑战。.NETCore就是这样一种受到广泛关注的解决方案。在本指南中,我们将深入研究.NETCore的基础知识,探索其架构、功......
  • CDS标准视图:银行对账单抬头 I_BankStatement
    视图名称:银行对账单抬头I_BankStatement视图类型:基础视图代码:点击查看代码事务代码:FF67/视图结构:字段名称技术名称短代码BANKSTATEMENTSHORTID公司代码COMPANYCODE开户行HOUSEBANK开户行账户标识HOUSEBANKACCOUNT银行对账单编号BANKSTATEMEN......