С# 5 async CTP: почему внутреннее "состояние" установлено в 0 в сгенерированном коде перед вызовом EndAwait?

Вчера я рассказывал о новой функции "async" на С#, в частности вникая в то, что выглядел сгенерированный код, и вызовы the GetAwaiter()/BeginAwait()/EndAwait().

Мы подробно рассмотрели на машине состояний, сгенерированной компилятором С#, и были два аспекта, которые мы не могли понять:

  • Почему сгенерированный класс содержит метод Dispose() и переменную $__disposing, которая никогда не используется (и класс не реализует IDisposable).
  • Почему внутренняя переменная state установлена ​​на 0 перед любым вызовом EndAwait(), когда 0 обычно означает, что это "начальная точка входа".

Я подозреваю, что на первый вопрос можно ответить, сделав что-то более интересное в асинхронном методе, хотя, если у кого-то есть дополнительная информация, я был бы рад услышать его. Однако этот вопрос касается второго момента.

Вот очень простой пример кода:

using System.Threading.Tasks;

class Test
{
    static async Task<int> Sum(Task<int> t1, Task<int> t2)
    {
        return await t1 + await t2;
    }
}

... и здесь код, который генерируется для метода MoveNext(), который реализует конечный автомат. Это скопировано непосредственно из Reflector - я не исправил имена невыразимых переменных:

public void MoveNext()
{
    try
    {
        this.$__doFinallyBodies = true;
        switch (this.<>1__state)
        {
            case 1:
                break;

            case 2:
                goto Label_00DA;

            case -1:
                return;

            default:
                this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
                this.<>1__state = 1;
                this.$__doFinallyBodies = false;
                if (this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate))
                {
                    return;
                }
                this.$__doFinallyBodies = true;
                break;
        }
        this.<>1__state = 0;
        this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
        this.<a2>t__$await4 = this.t2.GetAwaiter<int>();
        this.<>1__state = 2;
        this.$__doFinallyBodies = false;
        if (this.<a2>t__$await4.BeginAwait(this.MoveNextDelegate))
        {
            return;
        }
        this.$__doFinallyBodies = true;
    Label_00DA:
        this.<>1__state = 0;
        this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();
        this.<>1__state = -1;
        this.$builder.SetResult(this.<1>t__$await1 + this.<2>t__$await3);
    }
    catch (Exception exception)
    {
        this.<>1__state = -1;
        this.$builder.SetException(exception);
    }
}

Это долго, но важными для этого вопроса являются следующие:

// End of awaiting t1
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

// End of awaiting t2
this.<>1__state = 0;
this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();

В обоих случаях состояние снова изменяется после того, как оно, очевидно, будет наблюдаться далее... так зачем же устанавливать его на 0? Если бы MoveNext() были снова вызваны в этой точке (либо непосредственно, либо через Dispose), это эффективно запустило бы асинхронный метод снова, что было бы совершенно неуместным, насколько я могу сказать... if и MoveNext() не является так как изменение состояния не имеет значения.

Является ли это просто побочным эффектом компилятора, повторно использующего код генерации блока итератора для async, где он может иметь более очевидное объяснение?

Важная оговорка

Очевидно, что это просто компилятор CTP. Я полностью ожидаю, что ситуация изменится до финальной версии - и, возможно, даже до следующей версии CTP. Этот вопрос никоим образом не пытается утверждать, что это недостаток в компиляторе С# или что-то в этом роде. Я просто пытаюсь выяснить, есть ли у меня тонкая причина, которую я пропустил:)

Ответ 1

Хорошо, у меня наконец есть реальный ответ. Я как-то справился сам, но только после того, как Lucian Wischik из VB часть команды подтвердила, что для этого действительно есть веская причина. Большое спасибо ему - и, пожалуйста, посетите

Значение имеет значение, если ожидание заканчивается в исключении, которое было обнаружено. Мы можем снова вернуться к тому же ожидающему заявлению, но мы не должны находиться в состоянии, означающем "я как раз собираюсь вернуться с этого ожидания", так как в противном случае все виды кода будут пропущены. Проще всего показать это на примере. Обратите внимание, что теперь я использую второй CTP, поэтому сгенерированный код немного отличается от заданного в вопросе.

Здесь асинхронный метод:

static async Task<int> FooAsync()
{
    var t = new SimpleAwaitable();

    for (int i = 0; i < 3; i++)
    {
        try
        {
            Console.WriteLine("In Try");
            return await t;
        }                
        catch (Exception)
        {
            Console.WriteLine("Trying again...");
        }
    }
    return 0;
}

Концептуально, SimpleAwaitable может быть любой ожидаемой - может быть, задача, может быть, что-то еще. Для целей моих тестов он всегда возвращает false для IsCompleted и генерирует исключение в GetResult.

Здесь сгенерированный код для MoveNext:

public void MoveNext()
{
    int returnValue;
    try
    {
        int num3 = state;
        if (num3 == 1)
        {
            goto Label_ContinuationPoint;
        }
        if (state == -1)
        {
            return;
        }
        t = new SimpleAwaitable();
        i = 0;
      Label_ContinuationPoint:
        while (i < 3)
        {
            // Label_ContinuationPoint: should be here
            try
            {
                num3 = state;
                if (num3 != 1)
                {
                    Console.WriteLine("In Try");
                    awaiter = t.GetAwaiter();
                    if (!awaiter.IsCompleted)
                    {
                        state = 1;
                        awaiter.OnCompleted(MoveNextDelegate);
                        return;
                    }
                }
                else
                {
                    state = 0;
                }
                int result = awaiter.GetResult();
                awaiter = null;
                returnValue = result;
                goto Label_ReturnStatement;
            }
            catch (Exception)
            {
                Console.WriteLine("Trying again...");
            }
            i++;
        }
        returnValue = 0;
    }
    catch (Exception exception)
    {
        state = -1;
        Builder.SetException(exception);
        return;
    }
  Label_ReturnStatement:
    state = -1;
    Builder.SetResult(returnValue);
}

Мне пришлось перемещать Label_ContinuationPoint, чтобы сделать его действительным кодом, иначе он не входит в объем оператора goto, но это не влияет на ответ.

Подумайте, что произойдет, когда GetResult выдает исключение. Мы пройдем через блок catch, increment i, а затем снова округлим цикл (при условии, что i все еще меньше 3). Мы все еще находимся в каком-либо состоянии перед вызовом GetResult... но когда мы попадаем в блок try, мы должны напечатать "In Try" и снова вызвать GetAwaiter... и мы будем делать только что если состояние не равно 1. Без назначения state = 0 он будет использовать существующий awaiter и пропустить вызов Console.WriteLine.

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

Ответ 2

если он был сохранен в 1 (первый случай), вы получили бы вызов EndAwait без вызова BeginAwait. Если он хранится в 2 (второй случай), вы получите тот же результат только на другом awaiter.

Я предполагаю, что вызов BeginAwait возвращает false, если он уже запущен (предположим с моей стороны) и сохраняет исходное значение для возврата в EndAwait. Если в этом случае он будет работать правильно, тогда как если вы установите его на -1, вы можете иметь неинициализированный this.<1>t__$await1 для первого случая.

Однако это предполагает, что BeginAwaiter фактически не начнет действие при любых вызовах после первого и что в этих случаях он вернет false. Конечно, начало было бы неприемлемым, поскольку оно могло иметь побочный эффект или просто дать другой результат. Он также предполагает, что EndAwaiter всегда будет возвращать одно и то же значение независимо от того, сколько раз он вызывал и что можно вызвать, когда BeginAwait возвращает false (согласно вышеприведенному допущению)

Казалось бы, это защита от условий гонки Если мы встраиваем утверждения, в которых movenext вызывается другим потоком после состояния = 0 в вопросах, он будет выглядеть примерно так:

this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)
this.<>1__state = 0;

//second thread
this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)
this.$__doFinallyBodies = true;
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

//other thread
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

Если вышеприведенные предположения верны, есть некоторая ненужная работа, такая как получение пилоратера и переназначение того же значения до < 1 > t __ $await1. Если состояние было сохранено в 1, то последняя часть была бы следующей:

//second thread
//I suppose this un matched call to EndAwait will fail
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

далее, если он был установлен в 2, конечный автомат предположил, что он уже получил значение первого действия, которое было бы неверным, и для вычисления результата

будет использоваться (потенциально) неназначенная переменная,

Ответ 3

Может ли это быть связано со сложными/вложенными асинхронными вызовами?..

то есть:

async Task m1()
{
    await m2;
}

async Task m2()
{
    await m3();
}

async Task m3()
{
Thread.Sleep(10000);
}

Вызывается ли делегат movenext несколько раз в этой ситуации?

Просто пунт действительно?

Ответ 4

Объяснение фактических состояний:

возможные состояния:

  • 0 Инициализировано (я так думаю) или ждет окончания операции
  • 0 просто называется MoveNext, выбирая следующее состояние
  • -1 закончился

Возможно ли, что эта реализация просто хочет заверить, что если произойдет другой вызов MoveNext из любой ситуации (в ожидании), он будет снова переоценивать целую цепочку состояний с самого начала, , чтобы переоценить результаты, которые могут быть в среднее время уже устарело?