【问题标题】:How does async-await not block?async-await 如何不阻塞?
【发布时间】:2013-12-13 18:36:40
【问题描述】:

我认为异步方法有利于 IO 工作,因为它们不会在等待线程时阻塞线程,但这实际上是怎么可能的呢?我认为必须有一些东西在监听才能触发任务完成,所以这是否意味着阻塞只是移动到了其他地方?

【问题讨论】:

    标签: c# async-await


    【解决方案1】:

    不,阻塞不会移动到其他任何地方。返回可等待类型的 BCL 方法使用重叠 I/O 与 I/O 完成端口等技术,以获得完全异步的体验。

    我有一个recent blog post,它描述了它是如何一直到物理设备并返回的。

    【讨论】:

    • 酷!当我想到这个问题时,我实际上正在阅读您的博客,这很有趣。看来我必须阅读您的所有帖子才能再次使用 stackoverflow!
    • @NickL,你并不孤单。 :)
    【解决方案2】:

    Async-await 实际上是为您重新编写代码。它的作用是使用任务延续并将该延续放回到创建延续时当前的同步上下文中。

    所以下面的函数

    public async Task Example()
    {
        Foo();
        string barResult = await BarAsync();
        Baz(barResult);
    }
    

    变成类似(但不完全是)这样的东西

    public Task Example()
    {
        Foo();
        var syncContext = SyncronizationContext.Current;
        return BarAsync().ContinueWith((continuation) =>
                        {
                            Action postback = () => 
                            {
                                string barResult = continuation.Result();
                                Baz(barResult)
                            }
    
                            if(syncContext != null)
                                syncContext.Post(postback, null);
                            else
                                Task.Run(postback);
                        });
    }
    

    现在它实际上比这要复杂得多,但这是它的基本要点。


    真正发生的是它调用函数GetAwaiter()(如果存在)并执行类似这样的操作

    public Task Example()
    {
        Foo();
        var task = BarAsync();
        var awaiter = task.GetAwaiter();
    
        Action postback = () => 
        {
             string barResult = awaiter.GetResult();
             Baz(barResult)
        }
    
    
        if(awaiter.IsCompleted)
            postback();
        else
        {
            var castAwaiter = awaiter as ICriticalNotifyCompletion;
            if(castAwaiter != null)
            {
                castAwaiter.UnsafeOnCompleted(postback);
            }
            else
            {
                var context = SynchronizationContext.Current;
    
                if (context == null)
                    context = new SynchronizationContext();
    
                var contextCopy = context.CreateCopy();
    
                awaiter.OnCompleted(() => contextCopy.Post(postback, null));
            }
        }
        return task;
    }
    

    这仍然不是确切发生的情况,但重要的是,如果awaiter.IsCompleted 为真,它将同步运行回发代码,而不是立即返回。

    很酷的是,你不需要等待一个Task,你可以await anything,只要它有一个名为GetAwaiter()的函数并且返回的对象可以满足以下签名

    public class MyAwaiter<TResult> : INotifyCompletion
    {
        public bool IsCompleted { get { ... } }
        public void OnCompleted(Action continuation) { ... }
        public TResult GetResult() { ... }
    }
    //or
    public class MyAwaiter : INotifyCompletion
    {
        public bool IsCompleted { get { ... } }
        public void OnCompleted(Action continuation) { ... }
        public void GetResult() { ... }
    }
    

    making my wrong answer even more wrong 的持续冒险中,这是编译器将我的示例函数转换为的实际反编译代码。

    [DebuggerStepThrough, AsyncStateMachine(typeof(Form1.<Example>d__0))]
    public Task Example()
    {
        Form1.<Example>d__0 <Example>d__;
        <Example>d__.<>4__this = this;
        <Example>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
        <Example>d__.<>1__state = -1;
        AsyncTaskMethodBuilder <>t__builder = <Example>d__.<>t__builder;
        <>t__builder.Start<Form1.<Example>d__0>(ref <Example>d__);
        return <Example>d__.<>t__builder.Task;
    }
    

    现在,如果您查看那里,您会发现没有对 Foo()BarAsync()Baz(barResult) 的引用,这是因为当您使用 async 时,编译器实际上会将您的函数转换为 state machine基于IAsyncStateMachine 接口。如果我们去看看,编译器生成了一个名为&lt;Example&gt;d__0的新结构体

    [CompilerGenerated]
    [StructLayout(LayoutKind.Auto)]
    private struct <Example>d__0 : IAsyncStateMachine
    {
        public int <>1__state;
        public AsyncTaskMethodBuilder <>t__builder;
        public Form1 <>4__this;
        public string <barResult>5__1;
        private TaskAwaiter<string> <>u__$awaiter2;
        private object <>t__stack;
        void IAsyncStateMachine.MoveNext()
        {
            try
            {
                int num = this.<>1__state;
                if (num != -3)
                {
                    TaskAwaiter<string> taskAwaiter;
                    if (num != 0)
                    {
                        this.<>4__this.Foo();
                        taskAwaiter = this.<>4__this.BarAsync().GetAwaiter();
                        if (!taskAwaiter.IsCompleted)
                        {
                            this.<>1__state = 0;
                            this.<>u__$awaiter2 = taskAwaiter;
                            this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, Form1.<Example>d__0>(ref taskAwaiter, ref this);
                            return;
                        }
                    }
                    else
                    {
                        taskAwaiter = this.<>u__$awaiter2;
                        this.<>u__$awaiter2 = default(TaskAwaiter<string>);
                        this.<>1__state = -1;
                    }
                    string arg_92_0 = taskAwaiter.GetResult();
                    taskAwaiter = default(TaskAwaiter<string>);
                    string text = arg_92_0;
                    this.<barResult>5__1 = text;
                    this.<>4__this.Baz(this.<barResult>5__1);
                }
            }
            catch (Exception exception)
            {
                this.<>1__state = -2;
                this.<>t__builder.SetException(exception);
                return;
            }
            this.<>1__state = -2;
            this.<>t__builder.SetResult();
        }
        [DebuggerHidden]
        void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
        {
            this.<>t__builder.SetStateMachine(param0);
        }
    }
    

    感谢ILSpy 的人们让他们的工具使用了一个库,您可以自己扩展和调用代码。为了得到上面的代码,我所要做的就是

    using System.IO;
    using ICSharpCode.Decompiler;
    using ICSharpCode.Decompiler.Ast;
    using Mono.Cecil;
    
    namespace Sandbox_Console
    {
        internal class Program
        {
            public static void Main()
            {
                AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(@"C:\Code\Sandbox Form\SandboxForm\bin\Debug\SandboxForm.exe");
                var context = new DecompilerContext(assembly.MainModule);
                context.Settings.AsyncAwait = false; //If you don't do this it will show the original code with the "await" keyword and hide the state machine.
                AstBuilder decompiler = new AstBuilder(context);
                decompiler.AddAssembly(assembly);
    
                using (var output = new StreamWriter("Output.cs"))
                {
                    decompiler.GenerateCode(new PlainTextOutput(output));
                }
            }
        }
    }
    

    【讨论】:

    • "好的,这只是意味着阻塞是在 BarAsync() 中完成的。"我知道你是对的,但你的答案并没有解释原因,而 Stephen Cleary 的答案是。
    • @m59 你刚刚放入代码块的东西都不是实际的代码块......
    • BarAsync 没有要求在内部使用阻塞,它可能只是你放在后台线程上需要很长时间来处理的东西。阻塞涉及停止代码并等待外部源,您可以这样做(例如 Stepen 谈到的 IO 完成端口),但这不是必需的。
    • @ScottChamberlain 对于这个问题,我认为阻塞后台线程仍然是阻塞的,即使当前线程可以继续。来自问题:“这是否意味着阻塞只是移动到其他地方?”例如,后台线程。 (但我认为这很快就会变成毫无意义的讨论。就像我说的,我确实认为你的回答是完全正确的。如果我问这个问题,它不会帮助我理解。但也许它确实有助于 OP 理解。:) )
    • @hvd 我第一次没有仔细阅读这个问题,我 100% 同意你的观点。我不会删除我的答案,因为我认为它包含有用的信息,但斯蒂芬挑衅地是这个答案的“正确”答案。我回答的是标题,而不是问题:(
    猜你喜欢
    • 1970-01-01
    • 2018-07-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-16
    • 1970-01-01
    • 2016-02-08
    • 1970-01-01
    相关资源
    最近更新 更多