Почему ключевое слово async генерирует перечислитель и дополнительную структуру при компиляции?

Если я создаю простой класс следующим образом:

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, имеет структуру, сгенерированную для него с помощью счетчика, машины состояния перечислителя и некоторых дополнительных материалов.

enter image description here

Почему компилятор генерирует структуру с именем TestClass+<TestMethod>d__0 с перечислителем для метода async?

Кажется, что генерирует больше ИЛ, чем то, что производит настоящий метод. В этом примере компилятор генерирует 35 строк IL для моего класса, тогда как он генерирует 81 строку IL для структуры. Это также увеличивает сложность скомпилированного кода и заставляет NDepend указывать его для нескольких нарушений правил.

Ответ 1

Это потому, что ключевые слова async и await - это просто синтаксический сахар для чего-то, называемого сопрограммы.

Нет никаких специальных инструкций IL для поддержки создания асинхронных методов. Вместо этого метод async можно рассматривать как своего рода конечный автомат каким-то образом.

Я попытаюсь сделать этот пример как можно короче:

[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 в вашем асинхронном методе, а конечный автомат становится намного сложнее.

Кстати, есть еще одна конструкция в С#, которая делает такую ​​вещь: ключевое слово yield. Это также генерирует машину состояний, и код выглядит очень похоже на то, что создает await.

Для дальнейшего чтения просмотрите этот CodeProject, который более глубоко изучает сгенерированный конечный автомат.

Ответ 2

Исходное генерирование кода для async было тесно связано с тем, что было в блоках перечислителя, поэтому они начали использовать тот же код в компиляторе для этих двух преобразований кода. С тех пор он немного изменился, но он по-прежнему имеет некоторые преимущества от оригинального дизайна (например, имя MoveNext).

Подробнее о частях, созданных компилятором, Блог блога Jon Skeet - лучший источник.