2014-12-20 3 views
10

Если я создаю простой класс, как следующее:Почему ключевое слово 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 принимает логическое значение и быть async Task имеет структура создается для него с интервьюером, состояние машины переписчик и некоторые дополнительные вещи.

enter image description here

Почему компилятор генерирует-структуру под названием TestClass+<TestMethod>d__0 с интервьюером для метода асинхронного?

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

+2

Код в async/await основан на [Шаблон переадресации Async Джеффа Рихтера] (http://channel9.msdn.com/Blogs/Charles/Jeffrey-Richter-and-his-AsyncEnumerator). Уже есть голос, предлагающий исправить это https://ndepend.uservoice.com/forums/226344-ndepend-user-voice/suggestions/6375659-exclude-compiler-generated-code-by-default. – Aron

+3

Так как работает async/await. То, что вы видите, - это [деталь реализации] (http://msdn.microsoft.com/en-us/magazine/hh456402.aspx) этой функции. –

+0

Это действительно подчеркивает важность того, чтобы вы не слепо использовали асинхронный доступ по всему миру, когда он не нужен. Это больше накладных расходов, чем я предполагаю, я ожидал. –

ответ

2

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

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

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

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

+0

Спасибо за подробные ответы, это действительно помогло прояснить ситуацию. Я также обязательно задержу ссылку на ссылку –

+0

В этом примере ваш метод DoWork() является упрощенным вариантом .MoveNext() в автоответчике async? –

+0

Правильно. Минус-оптимизация, отмена, обработка исключений, методы внутренней структуры. – Frank

3

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

Подробнее о компиляторе, Jon Skeet's blog series - лучший источник.

+0

Это была хорошая серия, спасибо за ссылку –

 Смежные вопросы

  • Нет связанных вопросов^_^