C # 5 async CTP: dlaczego wewnętrzny "state" jest ustawiony na 0 w generowanym kodzie przed wywołaniem EndAwait?

Wczoraj wygłaszałem wykład na temat nowej funkcji C# "async", w szczególności zagłębiając się w to, jak wyglądał wygenerowany kod i the GetAwaiter() / BeginAwait() / EndAwait() Telefony.

Przyjrzeliśmy się bliżej maszynie stanu generowanej przez kompilator C# i były dwa aspekty, których nie mogliśmy zrozumieć:]}

  • dlaczego generowana klasa zawiera metodę Dispose() i zmienną $__disposing, które nigdy nie wydają się być używane (a klasa nie implementuje IDisposable).
  • dlaczego wewnętrzna zmienna state jest ustawiona na 0 przed wywołaniem EndAwait(), Gdy 0 zwykle oznacza "to jest początkowy punkt wejścia".

Podejrzewam, że na pierwszy punkt można by odpowiedzieć robiąc coś ciekawszego w ramach metody async, chociaż jeśli ktoś ma jakieś dodatkowe informacje, chętnie to usłyszę. To pytanie dotyczy jednak bardziej drugiego punktu.

Oto bardzo prosty fragment przykładowego kodu:

using System.Threading.Tasks;

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

... a oto kod generowany dla metody MoveNext() implementującej maszynę stanową. To jest skopiowane bezpośrednio z Reflector - nie naprawiłem niewypowiedzianych nazw zmiennych:

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);
    }
}

Jest długa, ale ważne linie na to pytanie są następujące:

// 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();

W obu przypadkach stan zmienia się ponownie później, zanim zostanie zaobserwowany... po co w ogóle ustawiać na 0? Jeśli {[11] } zostaną wywołane ponownie w tym momencie (bezpośrednio lub przez Dispose), to skutecznie uruchom ponownie metodę asynchroniczną, co byłoby całkowicie niewłaściwe, o ile mogę powiedzieć... jeśli i MoveNext() nie nazywa się, zmiana stanu jest nieistotna.

Czy jest to po prostu efekt uboczny kompilatora wykorzystującego kod generowania bloków iteratora dla asynchronizacji, gdzie może mieć bardziej oczywiste Wyjaśnienie?

Ważne zastrzeżenie

Oczywiście jest to tylko kompilator CTP. W pełni oczekuję zmian przed finalnym wydaniem-a może nawet przed kolejnym wydaniem CTP. To pytanie w żaden sposób nie próbuje twierdzić, że jest to wada w kompilatorze C# czy coś w tym stylu. Po prostu próbuję się dowiedzieć, czy jest jakiś subtelny powód, za którym tęskniłem:)

Author: Doctor Jones, 2011-02-17

4 answers

W końcu mam prawdziwą odpowiedź. Jakoś to sobie wymyśliłem, ale dopiero po tym, jak Lucian Wischik z zespołu VB potwierdził, że naprawdę jest ku temu dobry powód. Wielkie dzięki dla niego-i proszę odwiedzić jego blog , który rządzi.

Wartość 0 jest tutaj wyjątkowa, ponieważ jest , a nie poprawnym stanem, w którym możesz być tuż przed await w normalnym przypadku. W szczególności nie jest to stan, dla którego maszyna stanowa może skończyć testowanie gdzie indziej. Uważam, że użycie dowolnej nie dodatniej wartości byłoby równie dobre: -1 nie jest używane do tego, ponieważ jest logicznie niepoprawne, ponieważ -1 zwykle oznacza "ukończone". Mógłbym argumentować, że nadajemy dodatkowy sens stanowi 0 w tej chwili, ale ostatecznie to nie ma znaczenia. Celem tego pytania było ustalenie, dlaczego państwo jest w ogóle ustanawiane.

Wartość ma znaczenie, jeśli oczekiwanie kończy się wyjątkiem, który jest przechwytywany. Możemy wrócić do tego samego oczekuj ponownie, ale nie możemy być w stanie oznaczającym "właśnie wracam z tego oczekiwania", ponieważ w przeciwnym razie wszystkie rodzaje kodu zostałyby pominięte. Najłatwiej jest to pokazać na przykładzie. Zauważ, że używam teraz drugiego CTP, więc wygenerowany kod jest nieco inny niż w pytaniu.

Oto metoda asynchroniczna:

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;
}

Koncepcyjnie SimpleAwaitable może być dowolne - może zadanie, może coś innego. Na potrzeby moich testów, to zawsze zwraca false dla IsCompleted i rzuca wyjątek w GetResult.

Oto wygenerowany kod dla 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);
}

Musiałem przesunąć Label_ContinuationPoint, aby nadać mu poprawny kod - w przeciwnym razie nie wchodzi on w zakres goto instrukcji-ale to nie ma wpływu na odpowiedź.

Pomyśl o tym, co się stanie, gdy GetResult rzuci swój wyjątek. Przejdziemy przez blok catch, increment i, a następnie ponownie pętlę (zakładając, że i jest nadal mniejsza niż 3). Wciąż jesteśmy w stanie były przed telefonem... ale kiedy wejdziemy do bloku try, musimy wydrukować "in Try" i ponownie wywołaćGetAwaiter... i zrobimy to tylko wtedy, gdy stan nie będzie 1. Bez przypisania state = 0, użyje istniejącego awaitera i pominie wywołanie Console.WriteLine.

To dość kręta część kodu do pracy, ale to tylko pokazuje rodzaje rzeczy, o których zespół musi myśleć. Cieszę się, że nie jestem odpowiedzialny za wdrożenie tego:)

 71
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-04-23 07:16:28

Gdyby była trzymana na 1 (pierwszy przypadek) otrzymałbyś połączenie do EndAwait bez połączenia do BeginAwait. Jeśli jest utrzymana na 2 (drugi przypadek) to uzyskasz ten sam wynik tylko na drugim.

Domyślam się, że wywołanie BeginAwait zwraca false, jeśli zostało już uruchomione (zgaduję z mojej strony) i zachowuje oryginalną wartość do powrotu na końcu. Jeśli tak jest, to będzie działać poprawnie, podczas gdy Jeśli ustawisz go na -1 możesz mieć niezainicjalizowaną this.<1>t__$await1 dla pierwszego przypadku.

To zakłada jednak, że BeginAwaiter nie uruchomi żadnej akcji po pierwszym wywołaniu i w takich przypadkach zwróci false. Rozpoczęcie byłoby oczywiście niedopuszczalne, ponieważ mogłoby mieć skutki uboczne lub po prostu dać inny wynik. Zakłada również, że EndAwaiter zawsze zwróci tę samą wartość bez względu na to, ile razy zostanie wywołany, a to może być wywołane, gdy BeginAwait zwróci false (zgodnie z powyższym założeniem)

Wydaje się być strażnikiem przed rasą warunki Jeśli wpiszemy polecenia, w których movenext jest wywoływany przez inny wątek po state = 0 w pytaniach, będzie to wyglądało mniej więcej tak jak poniżej

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();

Jeśli powyższe założenia są poprawne, to wykonywana jest niepotrzebna praca, np. get sawiater i przypisanie tej samej wartości do t_ _ $await1. Jeśli stan był utrzymywany na poziomie 1, to ostatnia część byłaby w miejsce:

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

Dalej, gdyby była ustawiona na 2, maszyna stanowa założyłaby, że już otrzymała wartość pierwsza akcja, która byłaby nieprawdziwa i (potencjalnie) nieprzypisana zmienna byłaby używana do obliczenia wyniku

 5
Author: Rune FS,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-18 11:19:13

Czy to może mieć coś wspólnego ze stosowanymi/zagnieżdżonymi wywołaniami asynchronicznymi ?..

I. E:

async Task m1()
{
    await m2;
}

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

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

Czy delegat movenext jest wywoływany wiele razy w tej sytuacji ?

Naprawdę?
 1
Author: GaryMcAllister,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-17 14:41:08

Wyjaśnienie Stanów rzeczywistych:

Możliwe Stany:

  • 0 Initialized (I think so) or waiting for end of operation
  • >0 po prostu wywołujemy MoveNext, wybierając Następny stan
  • -1 ended

Czy jest możliwe, że ta implementacja chce tylko zapewnić, że jeśli kolejne wywołanie MoveNext z dowolnego miejsca (podczas oczekiwania) ponownie oceni cały łańcuch Stanów od początku, aby ponownie ocenić wyniki, które mogą być w międzyczasie już przestarzałe?

 0
Author: fixagon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-03 00:03:45