C # 5 zaman uyumsuz CTP: EndAwait çağrısından önce dahili “durum” neden oluşturulan kodda 0 olarak ayarlanmış?


195

Dün, özellikle üretilen kodun neye benzediğini ve the GetAwaiter()/ BeginAwait()/ EndAwait()çağrılarını inceleyerek yeni C # "async" özelliği hakkında bir konuşma yapıyordum .

C # derleyicisi tarafından oluşturulan durum makinesine biraz ayrıntılı baktık ve anlayamadığımız iki nokta vardı:

  • Oluşturulan sınıfın neden hiç kullanılmadığı (ve sınıfın uygulanmadığı ) bir Dispose()yöntem ve $__disposingdeğişken içerdiği IDisposable.
  • stateHerhangi bir çağrıdan önce dahili değişken neden 0 olarak ayarlanır EndAwait(), 0 normalde "bu ilk giriş noktasıdır" anlamına gelir.

İlk noktanın async yöntemi içinde daha ilginç bir şey yaparak cevaplanabileceğinden şüpheleniyorum, ancak herhangi bir başka bilgi varsa bunu duymaktan memnuniyet duyarım. Ancak bu soru daha çok ikinci konu ile ilgilidir.

İşte çok basit bir örnek kod parçası:

using System.Threading.Tasks;

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

... ve MoveNext()durum makinesini uygulayan yöntem için üretilen kod . Bu doğrudan Reflektör'den kopyalanır - Açıklanamayan değişken adlarını düzeltmedim:

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

Uzun ama bu sorunun önemli satırları:

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

Her iki durumda da, daha sonra açıkça gözlemlenmeden önce devlet tekrar değiştirilir ... öyleyse neden onu 0 olarak ayarladınız? Eğer MoveNext()bu noktada tekrar çağrıldı (doğrudan veya üzeri Dispose) etkin ve eğer ... anlarım kadarıyla tamamen uygunsuz olacağını, yine zaman uyumsuz yöntemini başlayacaktı MoveNext() değildir denilen durum değişikliğinin alakasız.

Bu sadece daha açık bir açıklamaya sahip olabileceği asenkron için tekrarlayıcı blok oluşturma kodunu yeniden kullanan derleyicinin bir yan etkisi midir?

Önemli sorumluluk reddi

Açıkçası bu sadece bir CTP derleyicisidir. Son sürümden önce ve muhtemelen bir sonraki CTP sürümünden önce bir şeylerin tamamen değişmesini bekliyorum. Bu soru hiçbir şekilde bu C # derleyicisindeki bir kusur veya böyle bir şey olduğunu iddia etmeye çalışmamaktadır. Ben sadece bunun kaçırdığım ince bir nedeni olup olmadığını anlamaya çalışıyorum :)


7
VB derleyicisi benzer bir durum makinesi üretir (bunun olup olmadığını bilmiyorum, ancak VB'nin yineleyici blokları yoktu)
Damien_The_Unbeliever

1
@ Run: MoveNextDelegate, MoveNext'e atıfta bulunan bir temsilci alanıdır. Her seferinde bekleyiciye geçmek için yeni bir Eylem yaratmaktan kaçınılması gerektiğine inanıyorum.
Jon Skeet

5
Bence cevap: Bu bir CTP. Ekip için üst düzey bit bunu ortaya çıkardı ve dil tasarımı doğrulandı. Ve inanılmaz derecede çabuk yaptılar. Gönderilen uygulamanın (derleyicilerin MoveNext değil) önemli ölçüde farklı olmasını beklemelisiniz. Eric veya Lucian'ın burada derin bir şey olmadığı, sadece çoğu durumda önemli olmayan bir davranış / hata olduğu ve kimsenin fark etmediği bir cevapla geri döneceğini düşünüyorum. Çünkü bu bir CTP.
Chris Burrows

2
@Stilgar: Sadece ildasm ile kontrol ettim ve gerçekten bunu yapıyor.
Jon Skeet

3
@JonSkeet: Kimsenin cevapları nasıl yükselttiğine dikkat edin. % 99'umuz cevabın doğru olup olmadığını bile söyleyemeyiz.
the_drow

Yanıtlar:


71

Tamam, sonunda gerçek bir cevabım var. Kendi başıma çalıştım, ancak takımın VB kısmından Lucian Wischik'in gerçekten bunun için iyi bir neden olduğunu doğruladıktan sonra. Ona çok teşekkürler - ve lütfen sallanan blogunu ziyaret edin .

Buradaki 0 ​​değeri yalnızca özeldir, çünkü normal durumda daha önce olabileceğiniz geçerli bir durum değildirawait . Özellikle, devlet makinesinin başka bir yerde test yapabileceği bir durum değildir. Pozitif olmayan bir değer kullanmanın da işe yarayacağına inanıyorum: -1 bunun için mantıksal olarak yanlış olduğu için kullanılmaz , çünkü -1 normalde "bitmiş" anlamına gelir. Şu anda 0 durumuna ekstra bir anlam verdiğimizi iddia edebilirim, ama sonuçta gerçekten önemli değil. Bu sorunun amacı, devletin neden kurulduğunu bulmaktı.

Beklenen yakalanan bir istisna ile biterse değer önemlidir. Yine aynı beklenen ifadeye geri dönebiliriz, ancak devlette "Bu beklemeden geri gelmek üzereyim" anlamında olmamalıyız , aksi takdirde her türlü kod atlanır. Bunu bir örnekle göstermek en kolayı. Şimdi ikinci CTP kullanıyorum, bu nedenle oluşturulan kod sorudakinden biraz farklı olduğunu unutmayın.

İşte async yöntemi:

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

Kavramsal olarak, SimpleAwaitableherhangi bir beklenen olabilir - belki bir görev, belki başka bir şey. Testlerim için, her zaman için yanlış döndürür IsCompletedve içine bir istisna atar GetResult.

Şunun için oluşturulan kod 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_ContinuationPointGeçerli kod yapmak için taşımak zorunda - aksi takdirde ifade kapsamında değildir goto- ama bu cevap etkilemez.

GetResultİstisnasını attığında neler olduğunu düşünün . Yakalama bloğundan geçeceğiz, arttıracağız ive sonra tekrar döneceğiz i(hala 3'ten az olduğu varsayılarak ). Biz eskisi ne olursa olsun devlet hâlâ GetResultçağrı ... ama içine girince trybloğun biz gereken "Try olarak" yazdırmak ve çağrı GetAwaiteryeniden ... ve biz sadece yapacağız devlet 1. olmadan değilse ostate = 0 atama, mevcut awaiter kullanıp atlayacak Console.WriteLineçağrıyı.

Üzerinde çalışılması oldukça dolambaçlı bir kod parçası, ama bu sadece takımın düşünmesi gereken şeyleri gösteriyor. Bunu uygulamaktan sorumlu olmadığım için mutluyum :)


8
@Shekhar_Pro: Evet, bu bir goto. Sen gerektiğini bekliyoruz :) otomatik oluşturulan devlet makinelerinde git tabloların bol görmek için
Jon Skeet

12
@Shekhar_Pro: Manuel olarak yazılan kod içinde - kodun okunmasını ve takip edilmesini zorlaştırdığı için. Kimse otojenize kodu okur, ama benim gibi kodalar gibi kodalar hariç :)
Jon Skeet 23:11

Yani ne yok buna istisna sonra yeniden bekliyor zaman ne? Tekrar başlıyor muyuz?
yapılandırıcı

1
@configurator: Beklemede GetAwaiter'ı çağırıyor, bunu yapmasını beklediğim şey bu.
Jon Skeet

gotos her zaman kodu okumayı zorlaştırmaz. Aslında, bazen kullanımı mantıklıdır (söylemek için kutsallık, biliyorum). Örneğin, bazen birden çok iç içe döngü kırmanız gerekebilir. Ben daha az goto (ve IMO çirkin kullanımı) özelliği kullanılan anahtar deyimleri art arda neden olur. Ayrı bir notta, gotoların bazı programlama dillerinin birincil temelini oluşturduğu bir gün ve yaşı hatırlıyorum ve bu nedenle goto'dan bahsetmenin neden geliştiricileri titrettiğini tam olarak anlıyorum. Kötü kullanılırlarsa işleri çirkin yapabilirler.
Ben Lesh

5

1'de tutulduysa (ilk vaka) EndAwait,BeginAwait . Eğer 2'de tutulursa (ikinci vaka) aynı sonucu sadece diğer bekleyicide alırsınız.

BeginAwait çağrılması zaten başlatılmışsa (benim tarafımdan bir tahmin) yanlış döndürür ve EndAwait dönmek için orijinal değeri tutar tahmin ediyorum. Durum buysa, doğru çalışır, ancak -1 olarak ayarlarsanız this.<1>t__$await1ilk durum için başlatılmamış olabilirsiniz .

Ancak bu, BeginAwaiter'ın ilk aramadan sonra herhangi bir çağrıda eylemi gerçekten başlatmayacağını ve bu durumlarda false değerini döndüreceğini varsayar. Tabii ki, yan etkisi olabilir veya sadece farklı bir sonuç verebileceği için başlangıç ​​kabul edilemez. Ayrıca, kaç kez çağrıldığından bağımsız olarak EndAwaiter'ın her zaman aynı değeri döndüreceğini ve BeginAwait yanlış döndürdüğünde çağrılabileceğini varsayar (yukarıdaki varsayım uyarınca)

Yarış koşullarına karşı bir koruma gibi görünecektir. Eğer movenext'in farklı bir iş parçacığı tarafından çağrıldığı ifadeleri satır içi ifade edersek, sorularda aşağıdaki gibi görünecektir.

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

Yukarıdaki varsayımlar doğruysa, sawiater almak ve aynı değeri <1> t __ $ await1 olarak yeniden atamak gibi gereksiz bazı işler var. Devlet 1'de tutulursa, son kısım yerine:

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

ayrıca, eğer 2'ye ayarlanmışsa, devlet makinesi, gerçek olmayan ilk eylemin değerini zaten aldığını varsayar ve sonucu hesaplamak için (potansiyel olarak) atanmamış bir değişken kullanılır


Durumun 0'a atama ile daha anlamlı bir değere atama arasında kullanılmadığını unutmayın . Eğer yarış koşullarına karşı korunmak isteniyorsa, başka bir değerin, örneğin -2, MoveNext'in başlangıcında uygunsuz kullanımı tespit etmek için bir kontrol ile belirtmesini beklerdim. Tek bir örneğin aslında iki iş parçacığı tarafından bir anda kullanılmaması gerektiğini unutmayın - bu, sık sık "duraklatmayı" başaran tek bir senkronize yöntem çağrısının yanılsamasını vermek içindir.
Jon Skeet

@Jon Async davasında bir yarış durumu ile ilgili bir sorun olmamalı ama yineleme bloğunda olabilir ve geride bırakılabilir kabul ediyorum
Rune FS

@Tony: Sanırım bir sonraki CTP veya beta çıkana kadar bekleyeceğim ve bu davranışı kontrol edeceğim.
Jon Skeet

1

Yığılmış / iç içe geçmiş asenkron çağrılarla ilgili bir şey olabilir mi? ..

yani:

async Task m1()
{
    await m2;
}

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

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

Bu durumda movenext delegesi birden çok kez çağrılıyor mu?

Gerçekten bir punt mu?


Bu durumda üretilen üç farklı sınıf olacaktır. MoveNext()her birine bir kez çağrılacaktı.
Jon Skeet

0

Gerçek durumların açıklaması:

olası durumlar:

  • 0 Başlatıldı (sanırım) veya operasyonun bitmesini bekliyor
  • > 0 sadece MoveNext olarak adlandırılır, bir sonraki durumu seçer
  • -1 bitti

Bu uygulamanın sadece başka bir MoveNext Çağrısı nerede olursa olsun (beklerken) tüm devlet zincirini baştan tekrar değerlendireceğini ve bu arada zaten eskimiş olabilecek sonuçları yeniden değerlendireceğini garanti etmek istiyor mu?


Ama neden baştan başlamak istesin ki? Gerçekte olmasını istediğiniz şey neredeyse bu değildir - başka bir şeyin MoveNext'i çağırmaması gerektiği için bir istisna fırlatmak istersiniz .
Jon Skeet
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.