C # 5 асинхронная CTP: почему внутреннее «состояние» установлено в 0 в сгенерированном коде перед вызовом EndAwait? - PullRequest
190 голосов
/ 17 февраля 2011

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

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

  • Почему сгенерированный класс содержит метод 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), то он фактически снова запустил бы асинхронный метод, что было бы совершенно неуместно, насколько я могу сказать ... если и MoveNext() не вызывается, изменение состояния не имеет значения.

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

Важный отказ от ответственности

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

Ответы [ 4 ]

70 голосов
/ 23 апреля 2011

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

Значение 0 здесь только специальное, потому что оно не , действительное состояние, в котором вы можете находиться перед await в обычном случае. В частности, это не состояние, которое конечный автомат может в конечном итоге проверить в другом месте. Я считаю, что использование любого неположительного значения будет работать так же хорошо: -1 не используется для этого, так как логически неверно, так как -1 обычно означает «закончено». Я мог бы утверждать, что в данный момент мы придаем дополнительный смысл состоянию 0, но в конечном итоге это не имеет большого значения. Смысл этого вопроса состоял в том, чтобы выяснить, почему государство вообще устанавливается.

Значение имеет значение, если ожидание заканчивается исключением, которое перехвачено. В конечном итоге мы можем снова вернуться к тому же самому заявлению ожидания, но мы не должны находиться в состоянии, означающем «я вот-вот вернусь из этого ожидания», так как иначе все виды кода были бы пропускаются. Проще всего показать это на примере. Обратите внимание, что я сейчас использую второй 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, увеличиваем i, а затем снова зациклимся (предполагая, что i все еще меньше 3). Мы все еще находимся в том состоянии, в котором мы были до вызова GetResult ... но когда мы попадаем внутрь блока try, мы должны напечатать "In Try" и снова вызвать GetAwaiter ... и мы сделаем это только в том случае, если состояние не равно 1. Без назначения state = 0 он будет использовать существующего ожидающего и пропустит вызов Console.WriteLine.

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

5 голосов
/ 17 февраля 2011

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

Я предполагаю, что вызов 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();

Если приведенные выше предположения верны, выполняется некоторая ненужная работанапример, get sawiater и переназначение того же значения на <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, конечный автомат предполагал бы, что уже получил значение первого действия, которое будетневерная и (потенциально) неназначенная переменная будет использоваться для вычисления результата

1 голос
/ 17 февраля 2011

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

т.е.:

async Task m1()
{
    await m2;
}

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

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

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

Просто пунт на самом деле?

0 голосов
/ 03 марта 2011

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

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

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

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

...