Pull to refresh

Async/await и механизм реализации в C# 5.0

Reading time 20 min
Views 79K

Подробно о преобразовании асинхронного кода, осуществляемого компилятором


Механизм async реализован в компиляторе C# при поддержке со стороны библиотек базовых классов .NET. В саму исполняющую среду не пришлось вносить никаких изменений. Это означает, что ключевое слово await реализовано путем преобразования к виду, который мы могли бы написать и сами в предыдущих версиях C#. Для изучения генерируемого кода можно воспользоваться декомпилятором .NET Reflector или ILSpy. Это не только интересно, но и полезно для отладки, анализа производительности и других видов диагностики асинхронного кода.

Метод заглушка


Рассмотрим сперва простой пример асинхронного метода:
public async Task<Int32> MethodTaskAsync()
        {
            Int32 one = 33;
            await Task.Delay(1000);
            return one;
        }

Данный пример довольно прост, но достаточно практичен и удобен для объяснения основного принципа реализации async/await. Запустим ILSpy и изучим код, который автоматически формирует компилятор C#:
         [AsyncStateMachine(typeof(Program.<MethodTaskAsync>d__0))]
		public Task<int> MethodTaskAsync()
		{
			Program.<MethodTaskAsync>d__0 <MethodTaskAsync>d__ = new Program.<MethodTaskAsync>d__0();
			<MethodTaskAsync>d__.<>4__this = this;
			<MethodTaskAsync>d__.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
			<MethodTask>d__.<>1__state = -1;
			AsyncTaskMethodBuilder<int> <>t__builder = <MethodTaskAsync>d__.<>t__builder;
			<>t__builder.Start<Program.<MethodTaskAsync>d__0>(ref <MethodTaskAsync>d__);
			return <MethodTaskAsync>d__.<>t__builder.Task;
		}

Интересно, не правда ли? Ключевое слово async не оказывает никакого воздействия на то, как метод используется извне. Это заметно по тому, что сигнатура метода, сформированного компилятором, соответствует оригинальному методу за исключением слова async. В какой-то мере спецификатор async не считается частью сигнатуры метода, к примеру, когда речь заходит о переопределении виртуальных методов, реализации интерфейса или вызове.

Единственное назначение ключевого слова async — изменить способ компиляции соответствующего метода, на взаимодействие с окружением оно не оказывает никакого влияния. Также отметим, что в «новом» методе нет следов оригинального кода.

Структура конечного автомата


В приведенном выше примере компилятор автоматически применил к методу атрибут AsyncStateMachine. Когда метод (MethodTaskAsync) имеет модификатор async, компилятор генерирует IL включающий структуру конечного автомата.

Эта структура содержит код в методе. Код IL также содержит метод–заглушку (MethodTaskAsync), вызываемый в конечном автомате. Компилятор добавляет атрибут AsyncStateMachine к методу–заглушке, чтобы можно было идентифицировать соответствующий конечный автомат. Это необходимо для того, чтобы сделать объект, способный сохранить состояние метода в тот момент, когда программа дойдет до await. Ведь, как известно, код до этого ключевого слова выполняется в вызывающем потоке, а затем при его достижении сохраняется информация о том, в каком месте метода находилась программа, чтобы при возобновлении программа могла продолжить выполнение.

Компилятор мог бы поступить и по-другому: просто сохранить все переменные метода. Но в таком случае пришлось бы сгенерировать много кода. Однако, можно поступить иначе, а именно — просто создать экземпляр некоторого типа и сохранить все данные метода в виде членов данного объекта. Тогда при сохранении данного объекта автоматически будут сохранены все локальные переменные метода. Для этого и предназначена формируемая структура, называемая конечным автоматом (Finite State Machine).

Если вкратце, конечный автомат — это абстрактный автомат, число возможных внутренних состояний которого конечно. Грубо говоря, конечный автомат глазами пользователя – это черный ящик, в который можно что-то передать и что-то оттуда получить. Это очень удобная абстракция, которая позволяет скрыть сложный алгоритм, кроме того конечные автоматы очень эффективны. При этом имеется конечное множество входных символов, из которого формируются выходные слова. Также следует учитывать то, что каждый входной символ переводит автомат в новое состояние. В нашем случае в качестве входных символов будет выступать состояние выполнения асинхронной операции и уже на основе этого значения конечный автомат будет формировать некоторое состояние и соответственно реакцию на выполнение задачи(выходное слово). Данный подход упрощает формирование и управления асинхронными задачами.Более подробно про конечные автоматы можно найти в интернете с кучей подробных статей.

Конечный автомат формируется в виде класса и содержит следующие переменные-члены:
    public int32 '<>1__state';
    private int32 '<one>5__1';
    public Mechanism_async.Program '<>4__this';
    public System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> '<>t__builder';
    private System.Runtime.CompilerServices.TaskAwaiter '<>u__awaiter';

Имена всех переменных содержат угловые скобки, показывающие, что имена сгенерированы компилятором. Это нужно для того, чтобы сгенерированный код не вступал в конфликт с пользовательским, — ведь в корректной программе на C# имена переменных не могут содержать угловых скобок.

  • В первой переменной <>1_state сохраняется номер достигнутого оператора await. Пока не встретился никакой await, значение этой переменной равно -1. Все операторы await в оригинальном методе пронумерованы, и в момент приостановки в переменную state заносится номер await, после которого нужно будет возобновить исполнение.
  • Следующая переменная <one>5_1 служит для хранения оригинальной переменной one. В генерированном компилятором коде все обращения к этой переменной заменены на обращение к этой переменной-члену.
  • Далее встречается переменная <>4_this. Она встречается только в конечных автоматах для нестатических асинхронных методов и содержит объект, от имени которого он вызывался. В каком-то смысле this — это просто еще одна локальная переменная метода, только она используется для доступа к другим переменным членам того же объекта. В процессе преобразования async-метода ее необходимо сохранить и использовать явно, потому что код оригинального объекта перенесен в структуру конечного автомата.
  • AsyncTaskMethodBuilder(<>t__builder) — представляет построитель для асинхронных методов, которые возвращают задачу. Этот вспомогательный тип и его члены предназначены для использования компилятором. Здесь инкапсулирована логика, общая для всех конечных автоматов. Именно этот тип создает объект Task, возвращаемых заглушкой. На самом деле этот тип очень похож на класс TaskCompletionSource в том смысле, что он создает задачу-марионетку, которую можно сделать завершенной позже. Отличие от TaskCompletionSource заключается в том, что AsyncTaskMethodBuilder оптимизирован для async-методов и ради повышения производительности является структурой, а не классом.
  • TaskAwaiter(<>u_awaiter) — здесь хранится временный объект, который ожидает завершения асинхронной задачи. Также представлен в виде структуры и помогает оператору await подписаться на уведомление о завершении задачи Task.

Для более детального изучения того, что же действительно происходит под капотом у компилятора рассмотрим IL кода, сформированного компилятором для <MethodTaskAsync>d__0:
IL-код
.class nested private auto ansi sealed beforefieldinit '<MethodTaskAsync>d__0'
	extends [mscorlib]System.Object
	implements [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine
{
	.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
		01 00 00 00
	)
	// Fields
	.field public int32 '<>1__state'
	.field public valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> '<>t__builder'
	.field public class Asynchronous.Program '<>4__this'
	.field private int32 '<one>5__1'
	.field private valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter '<>u__1'

	// Methods
	.method public hidebysig specialname rtspecialname 
		instance void .ctor () cil managed 
	{
		// Method begins at RVA 0x20ef
		// Code size 8 (0x8)
		.maxstack 8

		IL_0000: ldarg.0
		IL_0001: call instance void [mscorlib]System.Object::.ctor()
		IL_0006: nop
		IL_0007: ret
	} // end of method '<MethodTaskAsync>d__0'::.ctor

	.method private final hidebysig newslot virtual 
		instance void MoveNext () cil managed 
	{
		.override method instance void [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext()
		// Method begins at RVA 0x20f8
		// Code size 185 (0xb9)
		.maxstack 3
		.locals init (
			[0] int32,
			[1] int32,
			[2] valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter,
			[3] class Asynchronous.Program/'<MethodTaskAsync>d__0',
			[4] class [mscorlib]System.Exception
		)

		IL_0000: ldarg.0
		IL_0001: ldfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state'
		IL_0006: stloc.0
		.try
		{
			IL_0007: ldloc.0
			IL_0008: brfalse.s IL_000c

			IL_000a: br.s IL_000e

			IL_000c: br.s IL_0054

			IL_000e: nop
			IL_000f: ldarg.0
			IL_0010: ldc.i4.s 33
			IL_0012: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<one>5__1'
			IL_0017: ldc.i4 1000
			IL_001c: call class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Delay(int32)
			IL_0021: callvirt instance valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter [mscorlib]System.Threading.Tasks.Task::GetAwaiter()
			IL_0026: stloc.2
			IL_0027: ldloca.s 2
			IL_0029: call instance bool [mscorlib]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted()
			IL_002e: brtrue.s IL_0070

			IL_0030: ldarg.0
			IL_0031: ldc.i4.0
			IL_0032: dup
			IL_0033: stloc.0
			IL_0034: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state'
			IL_0039: ldarg.0
			IL_003a: ldloc.2
			IL_003b: stfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>u__1'
			IL_0040: ldarg.0
			IL_0041: stloc.3
			IL_0042: ldarg.0
			IL_0043: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>t__builder'
			IL_0048: ldloca.s 2
			IL_004a: ldloca.s 3
			IL_004c: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::AwaitUnsafeOnCompleted<valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter, class Asynchronous.Program/'<MethodTaskAsync>d__0'>(!!0&, !!1&)
			IL_0051: nop
			IL_0052: leave.s IL_00b8

			IL_0054: ldarg.0
			IL_0055: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>u__1'
			IL_005a: stloc.2
			IL_005b: ldarg.0
			IL_005c: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>u__1'
			IL_0061: initobj [mscorlib]System.Runtime.CompilerServices.TaskAwaiter
			IL_0067: ldarg.0
			IL_0068: ldc.i4.m1
			IL_0069: dup
			IL_006a: stloc.0
			IL_006b: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state'

			IL_0070: ldloca.s 2
			IL_0072: call instance void [mscorlib]System.Runtime.CompilerServices.TaskAwaiter::GetResult()
			IL_0077: nop
			IL_0078: ldloca.s 2
			IL_007a: initobj [mscorlib]System.Runtime.CompilerServices.TaskAwaiter
			IL_0080: ldarg.0
			IL_0081: ldfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<one>5__1'
			IL_0086: stloc.1
			IL_0087: leave.s IL_00a3
		} // end .try
		catch [mscorlib]System.Exception
		{
			IL_0089: stloc.s 4
			IL_008b: ldarg.0
			IL_008c: ldc.i4.s -2
			IL_008e: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state'
			IL_0093: ldarg.0
			IL_0094: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>t__builder'
			IL_0099: ldloc.s 4
			IL_009b: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::SetException(class [mscorlib]System.Exception)
			IL_00a0: nop
			IL_00a1: leave.s IL_00b8
		} // end handler

		IL_00a3: ldarg.0
		IL_00a4: ldc.i4.s -2
		IL_00a6: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state'
		IL_00ab: ldarg.0
		IL_00ac: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>t__builder'
		IL_00b1: ldloc.1
		IL_00b2: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::SetResult(!0)
		IL_00b7: nop

		IL_00b8: ret
	} // end of method '<MethodTaskAsync>d__0'::MoveNext

	.method private final hidebysig newslot virtual 
		instance void SetStateMachine (
			class [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine
		) cil managed 
	{
		.custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = (
			01 00 00 00
		)
		.override method instance void [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine::SetStateMachine(class [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine)
		// Method begins at RVA 0x21d0
		// Code size 1 (0x1)
		.maxstack 8

		IL_0000: ret
	} // end of method '<MethodTaskAsync>d__0'::SetStateMachine

} // end of class <MethodTaskAsync>d__0


Метод MoveNext


Класс, который был сформирован для типа MethodTask реализует интерфейс IAsyncStateMachine, который представляет конечные автоматы, созданные для асинхронных методов. Этот тип предназначен только для использования компилятором. Данный интерфейс содержит следующие члены: MoveNext и SetStateMachine. Метод MoveNext перемещает конечный автомат к следующему его состоянию. Этот метод содержит в себе оригинальный код и вызывается как при первом входе в метод, так и после await. Как считается, любой конечный автомат начинает свою работу в некотором начальном состоянии. Даже в случае простейшего async-метода код MoveNext на удивление сложен, поэтому я попытаюсь описать его и представить как можно точнее в эквиваленте на C#.

Метод MoveNext назван так из-за сходства с методами MoveNext, которые генерировались блоками итераторов в предыдущих версиях C#. Эти блоки позволяют реализовать интерфейс IEnumerable в одном методе с помощью ключевого слова yield return. Применяемый для этой цели конечный автомат во многом напоминает асинхронный автомат, только проще.

Рассмотрим промежуточный код и разберем, что же в нем происходит(хочу заметить, что ниже я решил полностью описать язык CIL для более полного рассмотрения того, что генерирует компилятор, а также описал все инструкции, так что кому не интересны технические подробности можете пропустить):
.locals init (
			[0] int32,
			[1] int32,
			[2] valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter,
			[3] class Asynchronous.Program/'<MethodTaskAsync>d__0',
			[4] class [mscorlib]System.Exception
		)


  • localsinit — это флаг, который устанавливается для метода и служит для инициализации локальных экземпляров типов значений. Он определен в заголовке метода и означает, что переменные должны быть проинициализированы в CIL. Здесь определены все экземпляры, которые будут использоваться в данном методе, причем они определены в виде массива и устанавливаются по умолчанию, как это принято: значение NULL для типов объектов и для полей типов значений, содержащих объекты, 0 для целых типов и 0.0 для типов с плавающей точкой.

    Таким образом при входе в метод мы уже имеет готовые локальные переменные для их использования в методе.
  • valuetype — означает, что это значимый тип, т.е. структура


                IL_0000: ldarg.0
		IL_0001: ldfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state'
		IL_0006: stloc.0

  • ldarg.0 — загружает аргумент 0 в стек. Аргумент с индексом 0 загружается в стек вычислений (промежуточный язык .NET является стековым), скопированный из входящего аргумента. Но у нас в методе нет аргументов! Дело в том, что по умолчанию в нестатических методах аргументом с индексом 0 всегда является указатель на экземпляр класса — this. Если же у вас при этом есть аргументы, то они будут уже иметь индекс 1, 2 и т.д. В статических метода ваши аргументы будут начинать отсчет с 0.
  • ldfld — выполняет поиск значения поля в объекте, ссылка на который находится в стеке. А сама ссылка была загружена выше при помощи ldarg.0, при этом значение, которое хранилось в этом поле соответственно загрузилось в стек.
  • stloc.0 — извлекает верхнее значение в стеке (это есть значение поля объекта MethodTaskAsync.state) и сохраняет его в списке локальных переменных с индексом 0. А список локальных переменных был еще объявлен в localsinit. Удобно, не правда ли?


                        IL_0007: ldloc.0
			IL_0008: brfalse.s IL_000c

			IL_000a: br.s IL_000e

			IL_000c: br.s IL_0054

			IL_000e: nop
			IL_000f: ldarg.0
			IL_0010: ldc.i4.s 33
			IL_0012: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<one>5__1'
			IL_0017: ldc.i4 1000
			IL_001c: call class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Delay(int32)
			IL_0021: callvirt instance valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter [mscorlib]System.Threading.Tasks.Task::GetAwaiter()
			IL_0026: stloc.2
			IL_0027: ldloca.s 2
			IL_0029: call instance bool [mscorlib]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted()
			IL_002e: brtrue.s IL_0070

  • ldloc.0 и brfalse.s — загружает локальную переменную с индексом 0 в стек вычислений. Здесь, только что сохраненное значение state загружается в стек, и команда brfalse.s передает управление конечной инструкции, если значением state является false, т.е. 0. При первом входе в метод, значение равно -1, значит выполнение потока инструкций идет дальше.
  • br.s IL_000e — безусловная передача конечной инструкции. Здесь выполнятся переход в другую часть кода, которую необходимо выполнить. В данном случае выполнение следующей команды будет происходить на строке IL_000e.
  • br.s IL_0054 — также безусловный переход, только эта команда выполнится, если выполнится команда brfalse.s
  • nop — Заполняет пространство, если коды операции содержат исправления. Никаких значимых операций не выполняется, хотя может быть пройден цикл обработки.
  • ldarg.0 и ldc.i4.s 33 — здесь происходит загрузка указателя this, а также загрузка числа 33 в стек, где ldc.i4.s — помещает значение с типом Int8 в стек вычислений как Int32 (краткая форма записи).
  • stfld — заменяет значение в поле объекта, по ссылке на объект на новое значение. С помощью загруженного указателя и числа 33 в стеке, в переменную член <one>5_1 (по умолчанию проинициализированной 0) загружается и сохраняется новое значение — 33. Как мы видим, это первая строка нашего исходного метода. Именно в этом блоке выполняется код оригинального метода.
  • ldc.i4 1000 — загрузка в стек переменной с типом Int32 как Int32.
  • call class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Delay(int32) — здесь происходит вызов метода. Особенностью этой инструкции (по сравнению с инструкцией callvirt) является то, что адрес вызываемого метода вычисляется статически, то есть еще во время JIT-компиляции. В данном случае метод Delay является статическим. При этом параметры вызываемого метода должны быть расположены в стеке слева направо, то есть сначала на стек должен быть загружен первый параметр, затем второй и т.д.
  • callvirt instance valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter [mscorlib]System.Threading.Tasks.Task::GetAwaiter() — эта инструкция отличается от call главным образом тем, что адрес вызываемого метода определяется во время выполнения программы путем анализа типа объекта, для которого вызывается метод. Тем самым реализуется идея позднего связывания, необходимая для поддержки полиморфизма. При этом возвращаемое значение (в данном случае TaskAwaiter) помещается в стек, где TaskAwaiter представляет объект, который ожидает завершения асинхронной задачи.
  • stloc.2 — извлекает верхнее значение в стеке и сохраняет его в списке локальных переменных с индексом 2. При этом следует учесть, что верхнее значение в стеке есть результат выполнения операции GetAwaiter() и соответственно сохраняется это значение в локальной переменной с индексом 2
  • ldloca.s 2 — загружает локальное значение с индексом 2 в стек — недавно сохраненное значение
  • call instance bool [mscorlib]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted() — загрузка в стек значения, указывающее, было ли выполнение задачи закончено на момент обращения к свойству: true или false
  • brtrue.s IL_0070 — если задача выполнена, переходим к выполнению другого участка кода, если нет, идем далее.

Таким образом можно представить код, аналогичный следующему:
public void MoveNext()
{
   switch(this.1_state)
   {
      case -1:
         this.one = 33;
         var task = Task.Delay(1000);
         var awaiter = task.GetAwaiter(); // стоит отметить, что это локальные переменные метода, а не самого типа (исходя из IL-кода)
   
         if(!awaiter.IsCompleted)
         {
            ...
            return;
         }
   }
   ...
      //рассмотрим далее
}

Код, представленный выше, отвечает за начальное состояние конечного автомата и проверяет завершенность асинхронной задачи и переходит в нужное место метода. При этом происходит переход в одно из нескольких состояний автомата: приостановка метода в месте встречи await или синхронное завершение.

Приостановка метода


Рассмотрим IL-код в месте приостановки метода:
                        IL_0030: ldarg.0
			IL_0031: ldc.i4.0
			IL_0032: dup
			IL_0033: stloc.0
			IL_0034: stfld int32 Asynchronous.Program/'<MethosTaskAsync>d__0'::'<>1__state'
			IL_0039: ldarg.0
			IL_003a: ldloc.2
			IL_003b: stfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'<MethosTaskAsync>d__0'::'<>u__1'
			IL_0040: ldarg.0
			IL_0041: stloc.3
			IL_0042: ldarg.0
			IL_0043: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> Asynchronous.Program/'<MethosTaskAsync>d__0'::'<>t__builder'
			IL_0048: ldloca.s 2
			IL_004a: ldloca.s 3
			IL_004c: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::AwaitUnsafeOnCompleted<valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter, class Asynchronous.Program/'<MethosTaskAsync>d__0'>(!!0&, !!1&)
			IL_0051: nop
			IL_0052: leave.s IL_00b8

Описывать каждую операцию не стоит, так как все описано уже выше.
  • Данный кусок кода отвечает за изменение переменной state на 0, где команда stfld int32 Asynchronous.Program/'d__0'::'<>1__state' повторюсь означает изменение значения поля на новое значение. А чтобы возобновиться с нужного места, необходимо изменить переменную state.
  • Затем используется объект TaskAwaiter для подписки на уведомление о завершение задачи Task. Это происходит при загрузке в стек локальной переменной с индексом 2 и изменением значения поля на значение этой локальной переменной(команды ldloc.2 и stfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'d__0'::'<>u__1'). Затем возвращается управление и освобождается поток для других дел, как и полагается приличному асинхронному методу.
  • В процедуре подписки на уведомление участвует также объект AwaitUnsafeOnCompleted. Именно здесь реализуются дополнительные возможности await, в том числе запоминание контекста синхронизации, который нужно будет восстановить при возобновлении. Этот метод планирует конечный автомат для перехода к следующему действию по завершении выполнения указанного объекта типа awaiter. В качестве параметров: AsyncTaskMethodBuilder.AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine). Как видно перед вызовом этого метода происходит загрузка в стек двух переменных с индексом 2 и 3, где 2 — valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter, 3 — class Asynchronous.Program/'d__0'

Рассмотрим немного подробнее структуру AsyncTaskMethodBuilder(сильно глубоко копать здесь не буду, потому что на мой взгляд изучение этой структуры и все, что с ней связано можно расписать как бы не на несколько статей):
         /// <summary>Кэшированная задача для default(TResult).</summary>
        internal readonly static Task<TResult> s_defaultResultTask = AsyncTaskCache.CreateCacheableTask(default(TResult));
  
        /// <summary>Состояние, связанное с IAsyncStateMachine.</summary>
        private AsyncMethodBuilderCore m_coreState; // mutable struct: must not be readonly
        /// <summary>Ленивая инициализация задачи</summary>
        private Task<TResult> m_task; // lazily-initialized: must not be readonly

        /// <summary>
        /// Планирует состояние данной машины для дальнейшего действия, когда awaiter выполнится /// </summary>
        /// <typeparam name="TAwaiter">Определяет тип awaiter.</typeparam>
        /// <typeparam name="TStateMachine">Определяет тип состояния машины.</typeparam>
        /// <param name="awaiter">The awaiter.</param>
        /// <param name="stateMachine">Состояние машины.</param>
        [SecuritySafeCritical]
        public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
            ref TAwaiter awaiter, ref TStateMachine stateMachine)
            where TAwaiter : ICriticalNotifyCompletion
            where TStateMachine : IAsyncStateMachine
        {
            try
            {
                AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize = null;
                var continuation = m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn ? this.Task : null, ref runnerToInitialize);
                
                // Если это первый await, то мы не упаковали состояние машины и должны сделать это сейчас
                if (m_coreState.m_stateMachine == null)
                {
                    // Действие задачи должно быть проинициализировано до первого приостановления 
                    var builtTask = this.Task;
 
                    //Упаковка состояния машины при помощи вызова internal-метода,
                    // где ссылка будет храниться в кеше. 
                    m_coreState.PostBoxInitialization(stateMachine, runnerToInitialize, builtTask);
                }
 
                awaiter.UnsafeOnCompleted(continuation);
            }
            catch (Exception e)
            {
                AsyncMethodBuilderCore.ThrowAsync(e, targetContext: null);
            }
        }

Рассмотрим вкратце, что внутри этой структуры:
  • s_defaultResultTask = AsyncTaskCache.CreateCacheableTask(default(TResult)) — здесь создается задача без реализации Dispose при указании специального флага DoNotDispose. Данный подход используется при создании задач для кеширования или повторного использования.
  • AsyncMethodBuilderCore m_coreState — представляет состояние, связанное с выполнением IAsyncStateMachine. Это структура.
  • AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize — предоставляет возможность вызова метода конечного автомата MoveNext согласно предоставленному контексту выполнения программы. Это структура, которая содержит контекст выполнения, состояние конечного автомата и метод для выполнения MoveNext.
  • m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn? this.Task: null, ref runnerToInitialize) — получает Action при ожидании метода UnsafeOnCompleted и происходит прикрепление как задачи продолжения. Здесь также происходит запоминание состояния машины и контекста выполнения.
  • awaiter.UnsafeOnCompleted(continuation) — планирует продолжение действий, которые будут вызваны, когда экземпляр завершит выполнение. При этом в зависимости от того, нужно ли нам восстанавливать контекст или нет, будет вызван метод MoveNext соответственно с контекстом да приостановки метода или же выполнение продолжится в контексте того потока, в котором происходило выполнение задачи.

Получаем уже немного другой исходный код:
public void MoveNext()
{
   switch(this.1_state)
   {
      case -1:
         this.one = 33;
         var task = Task.Delay(1000);
         var awaiter = task.GetAwaiter(); // стоит отметить, что это локальные переменные метода, а не самого типа (исходя из IL-кода)
   
         if(!awaiter.IsCompleted)
         {
            this.1_state = 0;
            this.u__awaiter = awaiter; //u__awaiter это тип TaskAwaiter
            t_builder.AwaitUnsafeOnCompleted(ref this.u_awaiter, ref <MethodTaskAsync>d__0);
            return;
         }
   }
   ...
      //рассмотрим далее
}

Возобновление метода


После выполнения этого куска кода, вызывающий поток уходит заниматься своими делами, а мы пока тем временем ожидаем завершения задачи. После того, как задача завершилась, заново вызывается метод MoveNext(при этом вызов метода AwaitUnsafeOnCompleted сделал все необходимое для работы). Рассмотрим IL-код, который вызывается при продолжении:
                        IL_0054: ldarg.0
			IL_0055: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>u__1'
			IL_005a: stloc.2
			IL_005b: ldarg.0
			IL_005c: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>u__1'
			IL_0061: initobj [mscorlib]System.Runtime.CompilerServices.TaskAwaiter
			IL_0067: ldarg.0
			IL_0068: ldc.i4.m1
			IL_0069: dup
			IL_006a: stloc.0
			IL_006b: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state'

			IL_0070: ldloca.s 2
			IL_0072: call instance void [mscorlib]System.Runtime.CompilerServices.TaskAwaiter::GetResult()
			IL_0077: nop
			IL_0078: ldloca.s 2
			IL_007a: initobj [mscorlib]System.Runtime.CompilerServices.TaskAwaiter
			IL_0080: ldarg.0
			IL_0081: ldfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<one>5__1'
			IL_0086: stloc.1
			IL_0087: leave.s IL_00a3

                        IL_00a3: ldarg.0
		        IL_00a4: ldc.i4.s -2
		        IL_00a6: stfld int32 Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>1__state'
		        IL_00ab: ldarg.0
		        IL_00ac: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> Asynchronous.Program/'<MethodTaskAsync>d__0'::'<>t__builder'
		        IL_00b1: ldloc.1
		        IL_00b2: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::SetResult(!0)
		        IL_00b7: nop

  • В первой части происходит загрузка аргумента с индексом 0 — это this, затем происходит поиск переменной TaskAwaiter <>u__1, сохранение ее значения в локальной переменной с индексом 2, а затем повторная инициализация. После этого происходит загрузка в стек значения -1 и сохранения этого значения в переменной 1__state.Таким образом выполняется сброс состояния задачи.
  • Во второй части происходит загрузка в стек локальной переменной awaiter и вызов GetResult(). Затем новая загрузка в стек локальной переменной и ее новая инициализация. Затем происходит загрузка в стек переменной 5__1 и сохранение ее в локальной переменной с индексом 1 и переход к другой команде.
  • В третий части загрузка в стек значения -2 и сохранение ее в переменной 1_state. Затем загрузка в стек переменной t_builder и вызов метода SetResult(one).

В результате приблизительный исходный код:
public void MoveNext()
{
   switch(this.1_state)
   {
      case -1:
         this.one = 33;
         var task = Task.Delay(1000);
         var awaiter = task.GetAwaiter(); // стоит отметить, что это локальные переменные метода, а не самого типа (исходя из IL-кода)
   
         if(!awaiter.IsCompleted)
         {
            this.1_state = 0;
            this.u__awaiter = awaiter; //u__awaiter это тип TaskAwaiter
            t_builder.AwaitUnsafeOnCompleted(ref this.u_awaiter, ref <MethodTaskAsync>d__0);
            return;
         }

       case 0:
          var awaiter = this.u_awaiter;
          this.u_awaiter = new System.Runtime.CompilerServices.TaskAwaiter();
          this.1_state = -1;
     
          awaiter.GetResult();
          awaiter = new System.Runtime.CompilerServices.TaskAwaiter();
          var one = this.<one>5_1;

          this.1_state = -2;
          this.t_builder.SetResult(one);
   }
}

Синхронное завершение


В случае синхронного завершения не следует останавливать и возобновлять метод. В таком случае просто необходимо проверить выполнение метода и перейти в нужное место при помощи оператора goto case:
public void MoveNext()
{
   switch(this.1_state)
   {
      case -1:
         this.one = 33;
         var task = Task.Delay(1000);
         var awaiter = task.GetAwaiter(); // стоит отметить, что это локальные переменные метода, а не самого типа (исходя из IL-кода)
   
         if(awaiter.IsCompleted)
         {
            goto case 0;
         }

       case 0:
          this.1_state = 0;
          ...
   }
}

Скомпилированный компилятором код хорош тем, что его никто не должен сопровождать, поэтому употреблять goto можно сколько душе угодно.

И напоследок...


В данной статье я опирался на одну из моих любимых книг по асинхронному программированию в C# 5.0 Алекса Дэвиса. Вообще советую ее всем прочитать, поскольку она небольшая (при желании можно прочитать за один день) и очень интересно и в меру подробно описывает механизм async/await в целом. При этом можно почитать и новичкам, там все очень просто написано(примеры из жизни и тому подобное). При это читая ее и параллельно изучая IL-код, я нашел небольшое расхождение с тем, что пишется в книге и есть на самом деле. Но думаю, что скорее всего дело в том, что скорее всего с тех пор подправили немного компилятор и он стал выдавать немного другие результаты. Но это не столь критично, чтобы сильно на этом зацикливаться. При этом в качестве исходного кода (для описания AsyncTaskMethodBuilder использовал данный ресурс: это если кому будет интересно копнуть еще глубже).
Tags:
Hubs:
+22
Comments 7
Comments Comments 7

Articles