Поля State Machine
На данный момент мы рассмотрели сгенерированный метод точки входа и то, как работает все в реализации MoveNext. Мы также кратко посмотрели на некоторые поля, определенные в машине состояний. Давайте подробнее рассмотрим их.
Для метода CopyStreamToStream, показанного ранее:
Вот поля, на которые мы наконец-то наткнулись:
Каждое из этих полей имеет свою специфику:
- <>1__state. Это поле "состояние" в "машине состояний". Оно определяет текущее состояние машины состояний и, что самое важное, что должно быть сделано в следующий раз, когда вызывается MoveNext. Если состояние равно -2, операция завершена. Если состояние равно -1, либо мы собираемся вызвать MoveNext в первый раз, либо код MoveNext в данный момент выполняется на некотором потоке. Если вы отлаживаете обработку асинхронного метода и видите состояние как -1, это означает, что где-то есть поток, который на самом деле выполняет код, содержащийся в методе. Если состояние равно 0 или больше, метод приостановлен, и значение состояния говорит вам, на каком await он приостановлен. Хотя это не жесткое правило (некоторые шаблоны кода могут запутать нумерацию), в общем случае присвоенное состояние соответствует 0-базовому номеру ожидания в порядке сверху вниз исходного кода. Так, например, если тело асинхронного метода было полностью:
И вы обнаружили, что значение состояния равно 2, что почти наверняка
означает, что асинхронный метод в данный момент приостановлен, ожидая
завершения задачи, возвращенной из C().
- <>t__builder. Это builder для машины состояний, например, AsyncTaskMethodBuilder для Task, AsyncValueTaskMethodBuilder<TResult> для ValueTask<TResult>, AsyncVoidMethodBuilder для асинхронного метода void, или любой другой конструктор, который был объявлен для использования через атрибут [AsyncMethodBuilder(...)], указанный на типе возврата асинхронного или переопределенный таким атрибутом на самом асинхронном методе. Как было обсуждено ранее, конструктор отвечает за жизненный цикл асинхронного метода, включая создание возвращаемой задачи, в конечном итоге завершение этой задачи, и служит посредником для приостановки, с кодом в асинхронном методе просит builder приостановить выполнение до тех пор, пока не завершится определенный ожидатель.
- source/desti. Это builder для машины состояний, например, AsyncTaskMethodBuilder для Task, AsyncValueTaskMethodBuilder<TResult> для ValueTask<TResult>, AsyncVoidMethodBuilder для асинхронного метода void, или любой другой конструктор, который был объявлен для использования через атрибут [AsyncMethodBuilder(...)], указанный на типе возврата асинхронного или переопределенный таким атрибутом на самом асинхронном методе. Как было обсуждено ранее, конструктор отвечает за жизненный цикл асинхронного метода, включая создание возвращаемой задачи, в конечном итоге завершение этой задачи, и служит посредником для приостановки, с кодом в асинхронном методе просит builderприостановить выполнение до тех пор, пока не завершится определенный ожидатель.
Компилятор будет генерировать эти поля в машину состояний:
Обратите внимание на отсутствие чего-либо, названного someArgument. Но если мы изменим асинхронный метод так, чтобы он действительно использовал аргумент каким-либо образом:
Он появляется:
- <>5__2;. Это буфер "локальный", который был поднят до поля, чтобы он мог выжить через точки ожидания. Компилятор старается довольно усердно избегать ненужного поднятия состояния. Обратите внимание, что есть еще один локальный в исходном коде, numRead, у которого нет соответствующего поля в машине состояний. Почему? Потому что это не необходимо. Этот локальный устанавливается в результате вызова ReadAsync и затем используется в качестве входных данных для вызова WriteAsync. Между этими двумя вызовами нет ожидания, через которое значение numRead нужно было бы сохранить. Точно так же, как компилятор JIT мог бы выбрать хранение такого значения полностью в регистре и никогда фактически не вытекать его на стек, компилятор C# может избежать поднятия этого локального до поля, поскольку ему не нужно сохранять его значение через любые ожидания. В общем случае, компилятор C# может исключить поднятие локальных переменных, если он может доказать, что их значение не нужно сохранять через ожидания.
- <>u__1 и <>u__2. В асинхронном методе есть два ожидания: одно для Task<int>, возвращаемого ReadAsync, и одно для Task, возвращаемого WriteAsync. Task.GetAwaiter() возвращает TaskAwaiter, а Task<TResult>.GetAwaiter() возвращает TaskAwaiter<TResult>, оба из которых являются отдельными структурными типами. Поскольку компилятору нужно получить эти ожидатели до ожидания (IsCompleted, UnsafeOnCompleted) и затем иметь доступ к ним после ожидания (GetResult), ожидатели должны быть сохранены. И поскольку они являются отдельными структурными типами, компилятору нужно поддерживать два отдельных поля для этого (альтернативой было бы упаковка их и иметь одно поле объекта для ожидателей, но это привело бы к дополнительным затратам на выделение). Компилятор будет стараться повторно использовать поля, когда это возможно. Если у меня есть:
Есть пять ожиданий, но участвуют только два разных типа ожидателей: три - это TaskAwaiter<int>, и два - это TaskAwaiter<bool>. Таким образом, в машине состояний оказывается только два поля ожидателей:
Затем, если я изменю мой пример на:
Тем не менее, участвуют только Task<int> и Task<bool>, но я действительно использую четыре разных структурных типа ожидателей, потому что ожидатель, возвращаемый вызовом GetAwaiter() на том, что возвращается ConfigureAwait, отличается от того, который возвращается Task.GetAwaiter()... Это снова очевидно из полей ожидателей, созданных компилятором:
Если вы начинаете задумываться о том, как оптимизировать размер, связанный с асинхронной машиной состояний, одним из возможных направлений может быть поиск возможности объединить типы ожидаемых объектов и, соответственно, объединить эти поля ожидателей.
В машине состояний могут быть определены и другие виды полей. Особо отметим, что вы можете увидеть некоторые поля, содержащие слово "wrap". Рассмотрим этот глупый пример:
Это приводит к созданию машины состояний с следующими полями:
До сих пор ничего особенного. Теперь переверните порядок выражений, которые добавляются:
При этом вы получите следующие поля:
Теперь у нас есть еще один: <>7__wrap1. Почему? Потому что мы вычислили значение DateTime.Now.Second, и только после его вычисления нам что-то нужно было в await, а значение первого выражения нужно сохранить, чтобы добавить его к результату второго. Таким образом, компилятору необходимо гарантировать, что временный результат этого первого выражения доступен для добавления к результату await, что означает, что ему необходимо передать результат выражения во временное поле, что он и делает с этим <>7__wrap1 полем. Если вы когда-нибудь обнаружите, что гипероптимизируете реализацию асинхронного метода, чтобы уменьшить объем выделяемой памяти, вы можете поискать такие поля и посмотреть, смогут ли небольшие изменения в источнике избежать необходимости разгрузки и, таким образом, избежать необходимости в таких временных объектах.
The end!
Приятного кодирования!