【问题标题】:How is a task with await executed with no additional threads如何在没有额外线程的情况下执行具有等待的任务
【发布时间】:2019-10-02 09:54:18
【问题描述】:

这是这个问题的后续问题:

If async-await doesn't create any additional threads, then how does it make applications responsive?

还有这篇博文:

http://blog.stephencleary.com/2013/11/there-is-no-thread.html

关于链接问题的接受答案。他描述了调用await 时发生的步骤。

他在第 3 步和第 4 步中写道,当这种情况发生时,当前线程返回到调用上下文,这使得消息循环可用于接收其他消息,从而使应用程序响应。然后在第 5 步中,他写道,我们调用 await 的任务现已完成,原始方法的其余部分在收到新消息后继续执行以继续该方法的其余部分。

我不明白的是,如果没有新线程并且原始线程正忙于调用上下文,那么该任务是如何开始执行的?

所以我转向博客文章,它描述了整个操作实际上是如何在低得多的级别上完成的(老实说,我真的不知道它们是如何工作的),然后只是一个通知它已经完成了。 .但是我不明白为什么突然在任务的情况下,你可以依靠其他硬件而不是CPU来进行计算,这使得不创建新线程成为可能。如果这个任务有点复杂怎么办计算,那么我们不需要CPU吗?然后我们回到我正在写的内容,当前线程已经忙于调用上下文......

【问题讨论】:

    标签: c# multithreading asynchronous async-await


    【解决方案1】:

    但我不明白为什么突然在任务的情况下,您可以依靠其他硬件而不是 CPU 来进行计算,这使得不创建新线程成为可能。

    因为 CPU 不仅是硬件设备集中可能的并行来源。

    线程在逻辑上与 CPU 相关,因此对于任何受 CPU 限制的工作负载,我们要么通过创建线程显式使用线程,要么使用线程池或Task.Run 等更高级别的机制或隐式使用线程 - 无论如何,每个应用程序都在某个默认线程内启动.

    但还有另一种操作——输入/输出操作,它意味着使用除 CPURAM 硬件设备,如磁盘、网络适配器、键盘、各种外围设备等。这些设备处理传入的数据并且异步输出 - 没有人知道您下次按下一个键或新数据何时从网络到达。为了处理异步,这些硬件设备能够在没有 CPU 参与的情况下传输数据(简单来说,设备在 RAM 中提供了一些数据所在的地址,然后它可以自己进行传输)。这是对所发生情况的非常简单的描述,但您可以认为大多数异步流程都以这种方式结束。如您所见,那里不需要 CPU,因此无需创建新线程。至于入站数据,机制非常相似,唯一不同的是,一旦数据到达,它就会被放入特定的 RAM 区域以供进一步使用。当设备完成数据转换(入站或出站)时,它会引发称为中断的特定信号以通知 CPU 操作完成,并且 CPU 通过触发通常驻留在硬件设备驱动程序中的特定代码执行来对中断做出反应 - 以这种方式驱动程序可以向上级发送通知。中断可能来自异步设备,CPU 有义务暂停当前正在执行的任何当前执行并切换到中断处理程序。当设备驱动程序正在执行中断处理程序时,它会将有关 I/O 完成的通知发送到更高级别的 OS 堆栈,最后此通知会到达启动 I/O 操作的应用程序。它的完成方式主要取决于运行应用程序的操作系统。对于 Windows,有一个称为 I/O Completion Ports 的特定机制,这意味着使用一些线程池来处理 I/O 完成通知。这些通知最终来自 CLR 到应用程序并触发继续执行,最终可以在单独的线程上运行,该线程可以是来自 I/O 线程池的线程或任何其他线程,具体取决于特定的 awaiter 实现。

    至于您所指的文章的总体思路,可以改写为:async/await 后面没有线程,除非您明确创建它,因为await/async 本身只是高级通知框架,它可以扩展为与下面的任何异步机制一起工作。

    【讨论】:

    【解决方案2】:

    我不明白的是,如果没有新线程并且原始线程正忙于调用上下文,那么该任务是如何开始执行的?

    需要注意的是two types of tasks: what I call Delegate Tasks and Promise Tasks。委托任务是并行处理中使用的任务类型 - 原始任务并行库的使用风格,其中任务表示要执行的一些代码量。

    另一方面,Promise 任务仅提供某事已完成的通知(以及该操作的结果/异常)。 Promise 任务不执行代码;它们是回调的对象表示。 async 总是使用 Promise 任务。

    因此,当 async 方法返回 Task 时,该任务就是 Promise 任务。 It doesn't "execute", but it can "complete".

    【讨论】:

    • 那么什么执行承诺任务的代码?
    • @YonatanNir:Promise 任务没有代码;它们代表async 方法的完成。 async 方法中每个 await 之后的代码 - 称为“延续” - 将 run on a context captured by await
    【解决方案3】:

    我认为您需要了解编译器要达到的级别才能使async/await 代码正常工作。

    采取这种方法:

    public async Task<int> GetValue()
    {
        await Task.Delay(TimeSpan.FromSeconds(1.0));
        return 42;
    }
    

    编译后你会得到这个:

    [AsyncStateMachine(typeof(<GetValue>d__1))]
    public Task<int> GetValue()
    {
        <GetValue>d__1 stateMachine = default(<GetValue>d__1);
        stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
        stateMachine.<>1__state = -1;
        AsyncTaskMethodBuilder<int> <>t__builder = stateMachine.<>t__builder;
        <>t__builder.Start(ref stateMachine);
        return stateMachine.<>t__builder.Task;
    }
    
    [StructLayout(LayoutKind.Auto)]
    [CompilerGenerated]
    private struct <GetValue>d__1 : IAsyncStateMachine
    {
        public int <>1__state;
    
        public AsyncTaskMethodBuilder<int> <>t__builder;
    
        private TaskAwaiter <>u__1;
    
        private void MoveNext()
        {
            int num = <>1__state;
            int result;
            try
            {
                TaskAwaiter awaiter;
                if (num != 0)
                {
                    awaiter = Task.Delay(TimeSpan.FromSeconds(1.0)).GetAwaiter();
                    if (!awaiter.IsCompleted)
                    {
                        num = (<>1__state = 0);
                        <>u__1 = awaiter;
                        <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
                        return;
                    }
                }
                else
                {
                    awaiter = <>u__1;
                    <>u__1 = default(TaskAwaiter);
                    num = (<>1__state = -1);
                }
                awaiter.GetResult();
                result = 42;
            }
            catch (Exception exception)
            {
                <>1__state = -2;
                <>t__builder.SetException(exception);
                return;
            }
            <>1__state = -2;
            <>t__builder.SetResult(result);
        }
    
        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() 允许调用代码在遇到await 时退出。注意if (!awaiter.IsCompleted) 中的return;

    它只是一个可以完成所有魔法的状态机。

    【讨论】:

    • 我认为这会跳过 async / await 如何处理其他(现已删除)答案中存在的真正硬件级别的 IO 操作,例如文件访问。
    • @Enigmativity:虽然您的回答是正确的,但这里的问题是它可能无法解决 OPs 问题。这就是布拉德利所说的。是的,async/await 和 IO 是不相关的,但我们似乎相信 OP 询问如何在没有线程的情况下实现 async/await,即是什么导致继续“自行”自发地开始。如果这是 OP 关心的问题,那么状态机不是答案,但 IRQ 可能是 IO 案例的示例答案。
    • 没错!当启动执行磁盘 IO 的任务时,代码可以在从磁盘加载数据的同时继续运行。对于没有经验的用户来说,该程序似乎正在同时做两件事;必须涉及多个线程!当经验丰富的开发人员第一次接触异步/等待时,我什至遇到过这种反应。它可以在不产生额外线程的情况下全部工作这一事实非常不直观,并且在为什么一些开发人员最终不理解 async / await 或者只是简单地弄错了这一点中起着重要作用。
    • Wiktor 和 Bradley 对我的问题的看法是正确的。我可以接受在较低级别执行等待的任务,但是当我等待确实需要 CPU 执行其命令的任务时会发生什么?
    • @YonatanNir - 除非显式创建新线程,否则 CPU 绑定操作仍将阻塞当前线程。
    猜你喜欢
    • 1970-01-01
    • 2021-07-26
    • 1970-01-01
    • 2021-01-11
    • 2010-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多