C # 5 асинхронний CTP: чому для внутрішнього "стану" встановлено 0 в генерованому коді перед викликом EndAwait?


195

Вчора я розповідав про нову функцію C # "async", зокрема, заглиблюючись у те, як виглядав створений код, та 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 # або щось подібне. Я просто намагаюся розібратися, чи є тонка причина цього, що я пропустив :)


7
Компілятор VB виробляє аналогічну машину (не знаю, очікується це чи ні, але у VB раніше не було ітераторних блоків)
Damien_The_Unbeliever

1
@Rune: MoveNextDelegate - це лише поле делегата, яке посилається на MoveNext. Я вважаю, що це потрібно, щоб уникнути створення нової Дії, яка щоразу переходитиме до офіціанта.
Джон Скіт

5
Я думаю, що відповідь така: це CTP. Біт високого замовлення для команди отримував це там і мовний дизайн був підтверджений. І вони зробили це напрочуд швидко. Ви очікуєте, що відправлена ​​реалізація (компіляторів, а не MoveNext) суттєво відрізнятиметься. Я думаю, що Ерік або Лучан повернуться з відповіддю, що тут немає нічого глибокого, просто поведінка / помилка, яка не має значення в більшості випадків, і ніхто її не помітив. Тому що це CTP.
Кріс Берроуз

2
@Stilgar: Я щойно перевірив ілдазм, і справді це робиться.
Джон Скіт

3
@JonSkeet: Зауважте, як ніхто не підтримує відповіді. 99% з нас насправді не можуть сказати, чи відповідь звучить правильно.
the_drow

Відповіді:


71

Гаразд, я нарешті маю справжню відповідь. Я свого роду опрацював це самостійно, але лише після того, як Лучан Вищик з групи ВБ підтвердив, що для цього дійсно є вагомі причини. Велике спасибі йому - і, будь ласка, відвідайте його блог , який гойдає.

Значення 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викине його виняток. Ми пройдемо через блок лову, приріст i, а потім знову обведемо цикл (якщо припустити, що iце менше 3). Ми все ще перебуваємо в тому стані, в якому були до GetResultдзвінка ...try блоку, ми мусимо надрукувати "Спробувати" та зателефонувати GetAwaiterще раз ... і ми зробимо це лише в тому випадку, якщо стан не буде 1. Без у state = 0призначенні, він використовуватиме наявний офіціант і пропускатиме Console.WriteLineвиклик.

Це досить покрутливий шматочок коду, який потрібно працювати, але це просто показує речі, про які повинна думати команда. Я радий, що за це не несу відповідальності :)


8
@Shekhar_Pro: Так, це гото. Ви повинні очікувати, що ви побачите багато тверджень про гото в автогенерованих державних машинах :)
Джон Скіт

12
@Shekhar_Pro: Код, написаний вручну, це - тому що він робить код важким для читання та дотримання. Ніхто не читає автогенерований код, окрім дурнів, як я, які декомпілюють його :)
Джон Скіт

То що ж відбувається, коли ми знову чекаємо після винятку? Ми починаємо все заново?
конфігуратор

1
@configurator: Він називає GetAwaiter очікуваним, і саме це я б очікував.
Джон Скіт

gotos не завжди робить код важчим для читання. Насправді, іноді їх навіть має сенс використовувати (святох сказати, я знаю). Наприклад, іноді може знадобитися розірвати кілька вкладених циклів. Менш використовувана функція goto (і IMO більш погане використання) полягає в тому, щоб викликати заяви каскаду до каскаду. Як окремо, я пам’ятаю день і вік, коли готос був головною основою деяких мов програмування, і тому я цілком усвідомлюю, чому саме згадка про гото змушує розробників здригатися. Вони можуть зробити речі потворними, якщо використовувати їх погано.
Бен Леш

5

якби він зберігався в 1 (перший випадок), ви б дзвонили EndAwaitбез дзвінка на BeginAwait. Якщо він зберігається в 2 (другий випадок), ви отримаєте такий же результат лише в інших офіціантів.

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

Однак це передбачає, що BeginAwaiter насправді не запустить дію на будь-які дзвінки після першого і що в цих випадках він поверне помилкове. Починати, звичайно, було б неприйнятно, оскільки це може мати побічний ефект або просто дати інший результат. Також передбачається, що EndAwaiter завжди буде повертати одне і те ж значення незалежно від того, скільки разів воно викликається, і це може бути викликано, коли BeginAwait поверне помилкове (згідно з вищенаведеним припущенням)

Здавалося б, це захист від перегонових умов Якщо ми накреслимо висловлювання, у яких 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 __ $. Якби стан утримувався на рівні 1, замість нього остання частина:

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

далі, якщо вона була встановлена ​​на 2, державна машина вважає, що вона вже отримала значення першої дії, яке було б неправдивим, а для обчислення результату буде використана (потенційно) непризначена змінна.


Майте на увазі, що стан фактично не використовується між присвоєнням 0 та присвоєнням більш значущому значенню. Якщо це призначене для захисту від перегонових умов, я очікую, що якесь інше значення вкаже на це, наприклад, -2, з чеком цього на початку MoveNext, щоб виявити неналежне використання. Майте на увазі, що жоден екземпляр фактично ніколи не повинен використовуватися двома потоками одночасно - це покликане створити ілюзію єдиного синхронного виклику методу, який вдається "призупиняти" кожен так часто.
Джон Скіт

@Jon Я погоджуюся, що це не повинно бути проблемою з умовами перегонів у випадку async, але може бути в блоці ітерації і може бути залишеним
Rune FS

@Tony: Я думаю, я зачекаю, поки з'явиться наступний CTP або бета-версія, і перевіряю цю поведінку.
Джон Скіт

1

Невже це може бути пов’язано зі складеними / вкладеними викликами асинхронізації? ..

тобто:

async Task m1()
{
    await m2;
}

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

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

Чи в цій ситуації делегат movenext викликає кілька разів?

Тільки пунт насправді?


У цьому випадку було б три різних генерованих класи. MoveNext()буде називатися один раз на кожному з них.
Джон Скіт

0

Пояснення фактичних станів:

можливі стани:

  • 0 Ініціалізовано (я так думаю) або чекаю закінчення операції
  • > 0 щойно викликається MoveNext, вибираючи наступний стан
  • -1 закінчилося

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


Але навіщо це хочеться починати спочатку? Це майже точно не те, що ви насправді хотіли б статися - ви хочете, щоб виняток було викинуто, оскільки більше нічого не повинно викликати MoveNext.
Джон Скіт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.