【问题标题】:Why does the async keyword generate an enumerator & additional struct when compiled?为什么 async 关键字在编译时会生成枚举器和附加结构?
【发布时间】:2015-02-19 02:59:54
【问题描述】:

如果我创建一个如下所示的简单类:

public class TestClass
{
    public Task TestMethod(int someParameter)
    {
        return Task.FromResult(someParameter);
    }

    public async Task TestMethod(bool someParameter)
    {
        await Task.FromResult(someParameter);
    }
}

并在 NDepend 中检查它,它表明 TestMethod 采用 bool 并成为 async Task 具有为其生成的结构,其中包含枚举器、枚举器状态机和一些其他内容。

为什么编译器会生成一个名为 TestClass+<TestMethod>d__0 的结构体,其中包含用于异步方法的枚举器?

它产生的 IL 似乎比实际方法产生的要多。在这个例子中,编译器为我的类生成了 35 行 IL,而它为 struct 生成了 81 行 IL。它还增加了编译代码的复杂性,并导致 NDepend 将其标记为几个违反规则的行为。

【问题讨论】:

标签: c# asynchronous compiler-construction ndepend


【解决方案1】:

这是因为asyncawait 关键字只是coroutines 的语法糖。

没有特殊的 IL 指令来支持异步方法的创建。相反,异步方法可以被视为某种状态机。

我会尽量缩短这个例子:

[TestClass]
public class AsyncTest
{
    [TestMethod]
    public async Task RunTest_1()
    {
        var result = await GetStringAsync();
        Console.WriteLine(result);
    }

    private async Task AppendLineAsync(StringBuilder builder, string text)
    {
        await Task.Delay(1000);
        builder.AppendLine(text);
    }

    public async Task<string> GetStringAsync()
    {
        // Code before first await
        var builder = new StringBuilder();
        var secondLine = "Second Line";

        // First await
        await AppendLineAsync(builder, "First Line");

        // Inner synchronous code
        builder.AppendLine(secondLine);

        // Second await
        await AppendLineAsync(builder, "Third Line");

        // Return
        return builder.ToString();
    }
}

这是一些您可能已经习惯的异步代码:我们的GetStringAsync 方法首先同步创建StringBuilder,然后等待一些异步方法,最后返回结果。如果没有 await 关键字,这将如何实现?

将以下代码添加到AsyncTest 类中:

[TestMethod]
public async Task RunTest_2()
{
    var result = await GetStringAsyncWithoutAwait();
    Console.WriteLine(result);
}

public Task<string> GetStringAsyncWithoutAwait()
{
    // Code before first await
    var builder = new StringBuilder();
    var secondLine = "Second Line";

    return new StateMachine(this, builder, secondLine).CreateTask();
}

private class StateMachine
{
    private readonly AsyncTest instance;
    private readonly StringBuilder builder;
    private readonly string secondLine;
    private readonly TaskCompletionSource<string> completionSource;

    private int state = 0;

    public StateMachine(AsyncTest instance, StringBuilder builder, string secondLine)
    {
        this.instance = instance;
        this.builder = builder;
        this.secondLine = secondLine;
        this.completionSource = new TaskCompletionSource<string>();
    }

    public Task<string> CreateTask()
    {
        DoWork();
        return this.completionSource.Task;
    }

    private void DoWork()
    {
        switch (this.state)
        {
            case 0:
                goto state_0;
            case 1:
                goto state_1;
            case 2:
                goto state_2;
        }

        state_0:
            this.state = 1;

            // First await
            var firstAwaiter = this.instance.AppendLineAsync(builder, "First Line")
                                        .GetAwaiter();
            firstAwaiter.OnCompleted(DoWork);
            return;

        state_1:
            this.state = 2;

            // Inner synchronous code
            this.builder.AppendLine(this.secondLine);

            // Second await
            var secondAwaiter = this.instance.AppendLineAsync(builder, "Third Line")
                                            .GetAwaiter();
            secondAwaiter.OnCompleted(DoWork);
            return;

        state_2:
            // Return
            var result = this.builder.ToString();
            this.completionSource.SetResult(result);
    }
}

显然,第一个 await 关键字之前的代码保持不变。其他所有内容都转换为状态机,该状态机使用goto 语句分段执行您之前的代码。每完成一个等待的任务,状态机就会进入下一步。

为了阐明幕后发生的事情,这个例子过于简单化了。在你的异步方法中添加错误处理和一些foreach-Loops,状态机变得更加复杂。

顺便说一句,C# 中还有另一个结构可以做这样的事情:yield 关键字。这也会生成一个状态机,代码看起来与await 生成的非常相似。

如需进一步阅读,请查看this CodeProject,它更深入地了解了生成的状态机。

【讨论】:

  • 感谢您的详细回答,这确实有助于说清楚。我一定会检查链接以及
  • 在本例中,您的 DoWork() 方法是异步状态机中 .MoveNext() 的简化变体,对吧?
  • 没错。减去优化、取消、异常处理、内部框架方法。
【解决方案2】:

async 的原始代码生成与枚举块的生成密切相关,因此他们开始在编译器中使用相同的代码进行这两个代码转换。从那以后它发生了很大的变化,但它仍然有一些原始设计的保留(例如名称MoveNext)。

有关编译器生成部分的更多信息,Jon Skeet's blog series 是最好的来源。

【讨论】:

猜你喜欢
  • 2023-04-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-26
  • 1970-01-01
  • 2011-07-06
相关资源
最近更新 更多