65.9K
CodeProject 正在变化。 阅读更多。
Home

Async Await 和生成的 StateMachine

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2013 年 1 月 28 日

CPOL

3分钟阅读

viewsIcon

106671

显示 StateMachine 的内部结构。

引言

像大家一样,我深入研究了 .NET 4.5,想看看有什么新东西...但我没有找到关于状态机实际工作方式的任何好的逐步解释,所以我使用了 ilspy,并结合了 一个不错的视频 来自创建者,我对它是什么有了一个很好的概念。(也欢迎您来了解一下)。

状态机

我从某处读到你可以这样想

  • 调用 "before" 代码
  • 将 await 代码作为 task 调用
  • 将 "after" 代码存储在 continuation task 中

但事实并非如此。

真正发生的事情实际上是这样的

  • 在编译时
    • 生成一个名为 StateMachinestruct
    • 包含用于保存函数局部状态的字段
    • 创建一个 moveNext 函数,其中包含整个代码
    • 代码被 await 调用分割成几个 case (机器状态)
    • 一个创建并初始化该机器的调用代码替换了我们的 async 函数代码。
  • 在运行时:
    • 创建一个任务来运行机器代码
    • 局部变量被 "提升" 到状态机中作为字段。
    • 代码运行直到 await
    • 运行 awaited 函数的 task
    • 机器状态设置为下一个状态,以便下一个代码片段在唤醒时运行
    • 安排一个唤醒事件
    • MoveNext 函数返回 (线程被释放以执行其他操作(更新 UI))
  • 当操作系统发出唤醒调用时:
    • 调用处理 await continuation 的线程
    • CurrentSyncContext 用于选择正确的线程来运行它。
      • 可以使用以下方法更改此行为: await task.ConfigureAwait(false);
    • 下一个代码段运行,因为下一个状态在屈服控制之前已设置
    • 计划另一个 await,[等等]

在代码中看到它

解码了两个 async 函数

// AsyncAwait.Engine.Downloader
public static async Task<string> DownloadHtmlAsyncTask(string url)
{
    HttpClient httpClient = new HttpClient();
    Debug.WriteLine("before await");
    string result = await httpClient.GetStringAsync(url);
    Debug.WriteLine("after await");
    return result;
}

// AsyncAwait.ViewModel.MainWndViewModel
private async Task<string> DownloadWithUrlTrackingTaskAsync(string url)
{
    Debug.WriteLine("before await1");
    string Data = await DownloadHtmlAsyncTask(url);
    Debug.WriteLine("before await2");
    string Data1 = await DownloadHtmlAsyncTask(url);
    Debug.WriteLine("the end.");
    return Data;
}

反编译是使用 ilspy 完成的,其中“反编译 async 方法”已关闭。

第一个函数

DownloadHtmlAsyncTask 仅使用一个 await 调用。

这是初始化状态机的调用代码

[DebuggerStepThrough, AsyncStateMachine(typeof(AsyncMethods.<DownloadHtmlAsyncTask>d__0))]
public static Task<string> DownloadHtmlAsyncTask(string url)
{
    AsyncMethods.<DownloadHtmlAsyncTask>d__0 <DownloadHtmlAsyncTask>d__;
    <DownloadHtmlAsyncTask>d__.url = url;
    <DownloadHtmlAsyncTask>d__.<>t__builder = AsyncTaskMethodBuilder<string>.Create();

    //set initial machine state to -1
    <DownloadHtmlAsyncTask>d__.<>1__state = -1;
    AsyncTaskMethodBuilder<string> <>t__builder = <DownloadHtmlAsyncTask>d__.<>t__builder;
    <>t__builder.Start<AsyncMethods.<DownloadHtmlAsyncTask>d__0>(ref <DownloadHtmlAsyncTask>d__);
    return <DownloadHtmlAsyncTask>d__.<>t__builder.get_Task();

}

状态机定义:(我的评论内联

using ConsoleApplication1;
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[CompilerGenerated]
[StructLayout(LayoutKind.Auto)]
private struct <DownloadHtmlAsyncTask>d__0 : IAsyncStateMachine
{
    //initial state is set to -1 by the machine's creation code
    public int <>1__state;
    public AsyncTaskMethodBuilder<string> <>t__builder;
    public string url;
    public HttpClient <httpClient>5__1;
    public string <result>5__2;
    private TaskAwaiter<string> <>u__$awaiter3;
    private object <>t__stack;
    void IAsyncStateMachine.MoveNext()
    {
        string result;
        try
        {
            int num = this.<>1__state;

            //if (!Stop)
            if (num != -3)
            {
                TaskAwaiter<string> taskAwaiter;
                //machine starts with num=-1 so we enter
                if (num != 0)
                {
                    //first (+ initial) state code, run code before await is invoked
                    this.<httpClient>5__1 = new HttpClient();
                    Debug.WriteLine("before await");
                    
                    //a task is invoked
                    taskAwaiter = this.<httpClient>5__1.GetStringAsync(this.url).GetAwaiter();
                    
                    //[performance] check if this task has completed already,
                    //if it did, skip scheduling and boxing
                    if (!taskAwaiter.get_IsCompleted())
                    {
                        this.<>1__state = 0;
                        this.<>u__$awaiter3 = taskAwaiter;
                        
                        //Schedules the state machine to proceed
                        //to the next action when the specified awaiter completes.
                        //Also: sending this state machine here will trigger it's boxing into heap.
                        this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, 
                          AsyncMethods.<DownloadHtmlAsyncTask>d__0>(ref taskAwaiter, ref this);
                        
                        //release cpu knowing our next step will be called by Framework.
                        return;
                    }
                }
                else
                {
                    //set awaiter to null
                    taskAwaiter = this.<>u__$awaiter3;
                    this.<>u__$awaiter3 = default(TaskAwaiter<string>);
                    
                    //set state to initial state (temporarily)
                    this.<>1__state = -1;
                }
                
                //second (+ final) state code (state=0): set result to member, printout
                string arg_A5_0 = taskAwaiter.GetResult();
                
                //set awaiter to null
                taskAwaiter = default(TaskAwaiter<string>);
                
                //set StateMachine's result field, and end code (print out)
                string text = arg_A5_0;
                this.<result>5__2 = text;
                Debug.WriteLine("after await");
                
                //set return task's result
                result = this.<result>5__2;
            }
        }
        //exception handling is done here
        catch (Exception exception)
        {
            //set machine state to final state
            this.<>1__state = -2;
            this.<>t__builder.SetException(exception);
            return;
        }
        this.<>1__state = -2;
        this.<>t__builder.SetResult(result);
    }
    [DebuggerHidden]
    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
    {
        this.<>t__builder.SetStateMachine(param0);
    }
}
  • 状态机创建为一个 struct,这意味着它在堆栈上
  • 它被移动到 AwaitUnsafeOnCompleted 函数中的堆(更深入到机制中,它被强制转换为 IAsyncStateMachine,从而触发 boxing)。
  • 如果我们等待的任务在检查 taskAwaiter.IsCoplete 之前结束,则可以跳过此调用。

此函数只有两个代码片段(两个状态),因为只有一个 await 分隔代码,这可能是一个标准场景,但不是一个有趣的场景。

当 StateMachine 有更多状态时会发生什么?这使我们进入了第二个示例...

第二个函数

DownloadHtmlAsyncTask - 使用两个 await 调用

调用代码

[DebuggerStepThrough, AsyncStateMachine(typeof(AsyncMethods.<DownloadWithUrlTrackingTaskAsync>d__5))]
private Task<string> DownloadWithUrlTrackingTaskAsync(string url)
{
    AsyncMethods.<DownloadWithUrlTrackingTaskAsync>d__5 <DownloadWithUrlTrackingTaskAsync>d__;
    <DownloadWithUrlTrackingTaskAsync>d__.<>4__this = this;
    <DownloadWithUrlTrackingTaskAsync>d__.url = url;
    <DownloadWithUrlTrackingTaskAsync>d__.<>t__builder = AsyncTaskMethodBuilder<string>.Create();
    
    //set initial machine state to -1
    <DownloadWithUrlTrackingTaskAsync>d__.<>1__state = -1;
    AsyncTaskMethodBuilder<string> <>t__builder = 
      <DownloadWithUrlTrackingTaskAsync>d__.<>t__builder;
    <>t__builder.Start<AsyncMethods.<DownloadWithUrlTrackingTaskAsync>d__5>(ref <DownloadWithUrlTrackingTaskAsync>d__);
    return <DownloadWithUrlTrackingTaskAsync>d__.<>t__builder.get_Task();
}

状态机定义:(阅读代码中的注释

using ConsoleApplication1;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[CompilerGenerated]
[StructLayout(LayoutKind.Auto)]
private struct <DownloadWithUrlTrackingTaskAsync>d__5 : IAsyncStateMachine
{
    public int <>1__state;
    public AsyncTaskMethodBuilder<string> <>t__builder;
    public AsyncMethods <>4__this;
    public string url;
    public string <Data>5__6;
    public string <Data1>5__7;
    private TaskAwaiter<string> <>u__$awaiter8;
    private object <>t__stack;
    void IAsyncStateMachine.MoveNext()
    {
        string result;
        try
        {
            TaskAwaiter<string> taskAwaiter;
            //initialy state = -1, so we skip this at start
            switch (this.<>1__state)
            {
            case -3:
                //request machine to stop!
                //machine will go to end state and set result if one exists.
                goto IL_168;
            case 0:
                taskAwaiter = this.<>u__$awaiter8;
                this.<>u__$awaiter8 = default(TaskAwaiter<string>);
                //set state to initial state (temporarily)
                this.<>1__state = -1;
                goto IL_A1;
            case 1:
                taskAwaiter = this.<>u__$awaiter8;
                this.<>u__$awaiter8 = default(TaskAwaiter<string>);
                //set state to initial state (temporarily)
                this.<>1__state = -1;
                goto IL_121;
            }
            
            // first state code state=-1, 
                        //printout, and await, then return control to scheduler
            Debug.WriteLine("before await1");
            taskAwaiter = AsyncMethods.DownloadHtmlAsyncTask(this.url).GetAwaiter();
            if (!taskAwaiter.get_IsCompleted())
            {
                //set state to next step (0)
                this.<>1__state = 0;
                this.<>u__$awaiter8 = taskAwaiter;
                this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, 
                  AsyncMethods.<DownloadWithUrlTrackingTaskAsync>d__5>(ref taskAwaiter, ref this);
                return;
            }
            IL_A1:
            //second state code (state = 0), set the result of the first call to member, 
                        //printout, schedule next await, yield control
            string arg_B0_0 = taskAwaiter.GetResult();
            taskAwaiter = default(TaskAwaiter<string>);
            string text = arg_B0_0;
            this.<Data>5__6 = text;
            Debug.WriteLine("before await2");
            taskAwaiter = AsyncMethods.DownloadHtmlAsyncTask(this.url).GetAwaiter();
            if (!taskAwaiter.get_IsCompleted())
            {
                //set state to next step (1)
                this.<>1__state = 1;
                this.<>u__$awaiter8 = taskAwaiter;
                this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, 
                  AsyncMethods.<DownloadWithUrlTrackingTaskAsync>d__5>(ref taskAwaiter, ref this);
                return;
            }
            IL_121:
            //third state code (state = 1), 
                        //set the result of the first call to member, printout, 
                        //set the (function's) return task's result.
            string arg_130_0 = taskAwaiter.GetResult();
            taskAwaiter = default(TaskAwaiter<string>);
            text = arg_130_0;
            this.<Data1>5__7 = text;
            Debug.WriteLine("the end.");
            result = this.<Data>5__6;
        }
        catch (Exception exception)
        {
            //some exception handling: set end state (-2)
            this.<>1__state = -2;
            //set the exception in the builder
            this.<>t__builder.SetException(exception);
            return;
        }
        IL_168:
        //if no exception set end state and result
        this.<>1__state = -2;
        this.<>t__builder.SetResult(result);
    }
    [DebuggerHidden]
    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
    {
        this.<>t__builder.SetStateMachine(param0);
    }
}

关注点

  • 开销
    • 任何人都可以看到添加了“几个”额外的代码行。
    • 但根据微软的说法
      • 大约 40 个操作用于创建 async 状态机(微秒)。
      • 此开销与其他较旧的 async 机制相似或更少。
      • 只有在紧密循环中调用多个 awaits 时,这才会成为问题。
    • 除此之外,在今天的 CPU 上,当它在非 GUI 线程上运行时,谁会注意到开销?或者更好 - 在 HDD 控制器上...
  • 异常处理
    • Task.WhenAll 将抛出它聚合的任何任务中的任何异常。
    • 这意味着第一个异常将被抛出。
    • task.Exception 属性将包含 AggregateException 中的所有异常(您可以使用 Task.Wait() 而不是 await)
© . All rights reserved.