异步等待如何不阻塞?

14

我了解async方法对于IO操作很有用,因为它们在等待期间不会阻塞线程,但这究竟是如何实现的呢?我猜测必须有某个监听器来触发任务完成,那么这是否意味着阻塞只是被移动到其他地方了呢?

2个回答

20

不,阻塞操作没有移动到其他地方。返回可等待类型的BCL方法使用诸如重叠I/O和I/O完成端口等技术,以获得完全异步的体验。

我有一篇最近的博客文章,描述了这是如何一直运作到物理设备并返回的。


4
太酷了!有趣的是,我在看你的博客时想到这个问题。看来我得在去Stack Overflow之前读完你所有的文章了! - NickL
1
@NickL,你不是孤单的。 :) - William T. Mallard

12

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为真,则会同步运行回发代码而不仅仅是立即返回。很酷的是,您不需要等待一个任务,只要它有一个名为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() { ... }
}

让我的错误答案更加错误的持续冒险中,这是编译器将我的示例函数反编译成的实际代码。
[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时,编译器实际上将你的函数转换成了基于IAsyncStateMachine接口的状态机。如果我们去看一下,编译器生成了一个名为<Example>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的答案有。 - user743382
1
BarAsync 不要求在内部使用阻塞,它可以只是需要长时间处理的任务,你可以将其放在后台线程上。阻塞涉及停止代码并等待外部资源,你可以这样做(例如 Stepen 谈到的 IO 完成端口),但这不是必需的。 - Scott Chamberlain
1
@ScottChamberlain 在此问题中,我认为阻塞后台线程仍然是阻塞,即使当前线程可以继续。从问题中得知:“这是否意味着阻塞只是转移到其他地方?”例如,后台线程。(但我认为这可能很快变成无意义的讨论。就像我说的,我认为你的答案完全正确。如果我提出了这个问题,它对我理解没有帮助。但也许它确实能帮助提问者理解。 :) ) - user743382
2
@hvd 我第一次没有仔细阅读问题,我完全同意你的观点。我不会删除我的答案,因为我认为它包含了有用的信息,但在这个问题中Stephen明显是“正确”的答案。我回答了标题,而不是问题 :( - Scott Chamberlain
@hvd Scott并没有指阻止另一个BG线程,而仅是在另一个线程中执行CPU绑定工作,该工作在某个时刻会导致异步操作被标记为完成。 是的,在某个时候,线程总是在做一些工作(不像Stephen的例子中那样,没有勺子,我是说线程),但没有一个线程坐在那里无事可做,即阻塞,这确实使代码正确地异步化。 - Servy
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接