NET'te denetim akışı nasıl elde edilir ve bekler?


105

yieldAnahtar kelimeyi anladığım kadarıyla , eğer bir yineleyici bloğunun içinden kullanılırsa, çağıran koda kontrol akışını döndürür ve yineleyici tekrar çağrıldığında, kaldığı yerden devam eder.

Ayrıca, awaityalnızca aranan ucu beklemekle kalmaz, aynı zamanda kontrolü arayan kişiye geri verir, yalnızca arayan kişi awaitsyöntemi kullandığında kaldığı yerden devam eder .

Başka bir deyişle - iş parçacığı yoktur ve eşzamansız ve beklemenin "eşzamanlılığı", ayrıntıları sözdizimi tarafından gizlenen akıllı kontrol akışının neden olduğu bir yanılsamadır.

Şimdi, eski bir montaj programcısıyım ve talimat işaretçilerine, yığınlarına vb. Aşinayım ve normal kontrol akışlarının (alt rutin, özyineleme, döngüler, dallar) nasıl çalıştığını anlıyorum. Ama bu yeni yapılar-- onları anlamıyorum.

Bir awaitulaşıldığında, çalışma zamanı bundan sonra hangi kod parçasının çalıştırılması gerektiğini nasıl bilir? Ne zaman kaldığı yerden devam edebileceğini nasıl biliyor ve nerede olduğunu nasıl hatırlıyor? Mevcut çağrı yığınına ne oluyor, bir şekilde kaydediliyor mu? Ya çağırma yöntemi kendisinden önce başka yöntem çağrıları awaityaparsa - neden yığının üzerine yazılmaz? Ve bir istisna ve yığın çözülme durumunda çalışma zamanı tüm bunları nasıl halledebilir?

Ne zaman yieldulaşıldığında, çalışma zamanı eşyaların alınması gereken noktayı nasıl takip ediyor? Yineleyici durumu nasıl korunur?


4
TryRoslyn çevrimiçi derleyicisinde üretilen koda bir göz atabilirsiniz
xanatos

1
Jon Skeet tarafından yazılan Eduasync makale serisine göz atmak isteyebilirsiniz .
Leonid Vasilev

Yanıtlar:


115

Size özel sorularınızı aşağıda cevaplayacağım, ancak verimi nasıl tasarladığımız ve beklediğimizle ilgili kapsamlı makalelerimi okumanız iyi olur.

https://blogs.msdn.microsoft.com/ericlippert/tag/continuation-passing-style/

https://blogs.msdn.microsoft.com/ericlippert/tag/iterators/

https://blogs.msdn.microsoft.com/ericlippert/tag/async/

Bu makalelerin bazıları artık güncel değil; oluşturulan kod birçok yönden farklıdır. Ancak bunlar size kesinlikle nasıl çalıştığı konusunda fikir verecektir.

Ayrıca, lambdaların kapanış sınıfları olarak nasıl üretildiğini anlamıyorsanız, önce bunu anlayın . Lambdas'ınız yoksa, başları veya kuyrukları asenkron yapamazsınız.

Bir bekleme süresine ulaşıldığında, çalışma zamanı bundan sonra hangi kod parçasının çalıştırılması gerektiğini nasıl bilir?

await şu şekilde oluşturulur:

if (the task is not completed)
  assign a delegate which executes the remainder of the method as the continuation of the task
  return to the caller
else
  execute the remainder of the method now

Bu temelde bu. Beklemek sadece süslü bir dönüş.

Ne zaman kaldığı yerden devam edebileceğini nasıl biliyor ve nerede olduğunu nasıl hatırlıyor?

Eh, bunu nasıl yapacağım olmadan bekliyor? Foo yöntemi, yöntem çubuğunu çağırdığında, bir şekilde, çubuğun ne yaptığı önemli değil, foo'nun aktivasyonunun tüm yerelleri bozulmadan, foo'nun ortasına nasıl geri döneceğimizi hatırlıyoruz.

Assembler'da bunun nasıl yapıldığını biliyorsun. Foo için bir aktivasyon kaydı yığına itilir; yerlilerin değerlerini içerir. Çağrı noktasında, foo'daki dönüş adresi yığına itilir. Çubuk tamamlandığında, yığın işaretçisi ve komut işaretçisi olmaları gereken yere sıfırlanır ve foo, kaldığı yerden devam eder.

Beklemenin devamı tamamen aynıdır, tek fark, kaydın, etkinleştirme dizisinin bir yığın oluşturmaması gibi bariz bir nedenden ötürü yığına konulmasıdır .

Bekleyen delege, görevin devamı olarak (1) bir sonraki yürütmeniz gereken komut işaretçisini veren bir arama tablosunun girdisi olan bir sayı ve (2) yerellerin ve geçicilerin tüm değerlerini içerir.

Orada bazı ek donanımlar var; örneğin, .NET'te bir try bloğunun ortasına dalmak yasa dışıdır, bu nedenle kodun adresini bir try bloğunun içine tabloya yapıştıramazsınız. Ancak bunlar muhasebe detaylarıdır. Kavramsal olarak, aktivasyon kaydı basitçe yığına taşınır.

Mevcut çağrı yığınına ne oluyor, bir şekilde kaydediliyor mu?

Mevcut aktivasyon kaydındaki ilgili bilgiler asla ilk etapta yığına konulmaz; başlangıçtan itibaren yığın olarak tahsis edilir. (Biçimsel parametreler normalde yığına veya kayıtlara aktarılır ve daha sonra yöntem başladığında bir yığın konumuna kopyalanır.)

Arayanların aktivasyon kayıtları saklanmaz; Bekleme muhtemelen onlara geri dönecek, unutmayın, böylece normal şekilde ele alınacaktır.

Bunun, Scheme gibi dillerde gördüğünüz basitleştirilmiş devam ettirme bekleme stili ile güncel devamla arama yapıları arasındaki önemli bir fark olduğunu unutmayın. Bu dillerde, arayanlara geri devam etme dahil tüm devamlılık call-cc tarafından yakalanır .

Ya arama yöntemi beklemeden önce başka yöntem çağrıları yaparsa - neden yığının üzerine yazılmaz?

Bu yöntem çağrıları geri döner ve bu nedenle, etkinleştirme kayıtları bekleme noktasında artık yığın üzerinde değildir.

Ve bir istisna ve yığın çözülme durumunda çalışma zamanı tüm bunları nasıl halledebilir?

Yakalanmamış bir istisna durumunda, istisna yakalanır, görevin içinde saklanır ve görevin sonucu alındığında yeniden fırlatılır.

Daha önce bahsettiğim tüm muhasebe defterlerini hatırlıyor musun? İstisna anlambilimini doğru yapmak çok büyük bir acıydı, size söyleyeyim.

Verime ulaşıldığında, çalışma zamanı eşyaların nereden alınması gerektiğini nasıl takip ediyor? Yineleyici durumu nasıl korunur?

Aynı şekilde. Yerellerin durumu yığına taşınır ve bir MoveNextsonraki çağrıldığında devam etmesi gereken talimatı temsil eden bir sayı yerellerle birlikte depolanır.

Ve yine, istisnaların doğru bir şekilde ele alındığından emin olmak için bir yineleyici bloğunda bir sürü dişli var.


1
soru yazarlarının arka planı nedeniyle (assembler ve diğerleri), bu iki yapının da yönetilen bellek olmadan mümkün olamayacağından bahsetmeye değer. yönetilen bellek olmadan, bir kapanmanın ömrünü koordine etmeye çalışmak, önyüklemelerde kesinlikle tetiklenmenize neden olur.
Jim

Tüm sayfalara bağlantı bulunamadı (404)
Digital3D

Tüm makaleleriniz şu anda kullanılamıyor. Onları yeniden yayınlayabilir misin?
Michał Turczyn

1
@ MichałTurczyn: Hala internetteler; Microsoft, blog arşivinin olduğu yere taşınmaya devam ediyor. Bunları yavaş yavaş kişisel sitemin her yerine taşıyacağım ve zamanım olduğunda bu bağlantıları güncellemeye çalışacağım.
Eric Lippert

38

yield ikisinden daha kolay, o yüzden inceleyelim.

Elimizde diyelim:

public IEnumerable<int> CountToTen()
{
  for (int i = 1; i <= 10; ++i)
  {
    yield return i;
  }
}

Bu derlenmiş biraz biz yazılı olsaydık şöyle:

// Deliberately use name that isn't valid C# to not clash with anything
private class <CountToTen> : IEnumerator<int>, IEnumerable<int>
{
    private int _i;
    private int _current;
    private int _state;
    private int _initialThreadId = CurrentManagedThreadId;

    public IEnumerator<CountToTen> GetEnumerator()
    {
        // Use self if never ran and same thread (so safe)
        // otherwise create a new object.
        if (_state != 0 || _initialThreadId != CurrentManagedThreadId)
        {
            return new <CountToTen>();
        }

        _state = 1;
        return this;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public int Current => _current;

    object IEnumerator.Current => Current;

    public bool MoveNext()
    {
        switch(_state)
        {
            case 1:
                _i = 1;
                _current = i;
                _state = 2;
                return true;
            case 2:
                ++_i;
                if (_i <= 10)
                {
                    _current = _i;
                    return true;
                }
                break;
        }
        _state = -1;
        return false;
    }

    public void Dispose()
    {
      // if the yield-using method had a `using` it would
      // be translated into something happening here.
    }

    public void Reset()
    {
        throw new NotSupportedException();
    }
}

Bir elle yazılmış uygulanması Yani, verimli değildir IEnumerable<int>ve IEnumerator<int>(örneğin biz muhtemelen ayrı olan israf olmaz _state, _ive _currentgüvenli bir yeni oluşturmak yerine bu yüzden ziyade yapmak zaman bu durumda) hüner (ama kötü değil kendini yeniden kullanarak nesne iyidir) ve çok karmaşık yieldyöntemlerle başa çıkmak için genişletilebilir .

Ve tabii ki o zamandan beri

foreach(var a in b)
{
  DoSomething(a);
}

Aynıdır:

using(var en = b.GetEnumerator())
{
  while(en.MoveNext())
  {
     var a = en.Current;
     DoSomething(a);
  }
}

Daha sonra oluşturulan MoveNext()tekrar tekrar çağrılır.

Durum asynchemen hemen aynı prensiptir, ancak biraz daha karmaşıktır. Aşağıdaki gibi başka bir cevap Kodundaki bir örneği yeniden kullanmak için :

private async Task LoopAsync()
{
    int count = 0;
    while(count < 5)
    {
       await SomeNetworkCallAsync();
       count++;
    }
}

Şunun gibi kod üretir:

private struct LoopAsyncStateMachine : IAsyncStateMachine
{
  public int _state;
  public AsyncTaskMethodBuilder _builder;
  public TestAsync _this;
  public int _count;
  private TaskAwaiter _awaiter;
  void IAsyncStateMachine.MoveNext()
  {
    try
    {
      if (_state != 0)
      {
        _count = 0;
        goto afterSetup;
      }
      TaskAwaiter awaiter = _awaiter;
      _awaiter = default(TaskAwaiter);
      _state = -1;
    loopBack:
      awaiter.GetResult();
      awaiter = default(TaskAwaiter);
      _count++;
    afterSetup:
      if (_count < 5)
      {
        awaiter = _this.SomeNetworkCallAsync().GetAwaiter();
        if (!awaiter.IsCompleted)
        {
          _state = 0;
          _awaiter = awaiter;
          _builder.AwaitUnsafeOnCompleted<TaskAwaiter, TestAsync.LoopAsyncStateMachine>(ref awaiter, ref this);
          return;
        }
        goto loopBack;
      }
      _state = -2;
      _builder.SetResult();
    }
    catch (Exception exception)
    {
      _state = -2;
      _builder.SetException(exception);
      return;
    }
  }
  [DebuggerHidden]
  void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
  {
    _builder.SetStateMachine(param0);
  }
}

public Task LoopAsync()
{
  LoopAsyncStateMachine stateMachine = new LoopAsyncStateMachine();
  stateMachine._this = this;
  AsyncTaskMethodBuilder builder = AsyncTaskMethodBuilder.Create();
  stateMachine._builder = builder;
  stateMachine._state = -1;
  builder.Start(ref stateMachine);
  return builder.Task;
}

Daha karmaşık, ancak çok benzer bir temel ilke. Ekstra ana komplikasyon, şu anda GetAwaiter()kullanılmakta olmasıdır. Herhangi bir zaman awaiter.IsCompletedkontrol edilirse true, görev awaitzaten tamamlandığı için geri döner (örneğin, eşzamanlı olarak dönebileceği durumlar), o zaman yöntem durumlar arasında ilerlemeye devam eder, ancak aksi takdirde kendisini bekleyen için bir geri arama olarak ayarlar.

Bununla ne olacağı, geri aramayı neyin tetiklediği (ör. Eşzamansız G / Ç tamamlama, tamamlanan bir iş parçacığı üzerinde çalışan bir görev) ve belirli bir iş parçacığına sıralamak veya bir iş parçacığı iş parçacığı üzerinde çalıştırma için ne gibi gereksinimler olduğu açısından beklemeye bağlıdır. , orijinal aramadan hangi bağlamın gerekli olabileceği veya gerekmeyebileceği vb. Her ne olursa olsun, beklemedeki bir şey onu çağıracak MoveNextve ya bir sonraki iş parçasıyla devam edecek (bir sonrakine kadar await) ya da bitirip geri dönecek ve bu durumda Taskuyguladığı tamamlanmış olacaktır.


Kendi çevirinizi yapmak için zaman ayırdınız mı? O_O uao.
CoffeDeveloper

4
@DarioOO Çok hızlı bir şekilde yapabileceğim ilk şey, yield yapmanın bir faydası olduğunda (genellikle bir optimizasyon olarak, ancak başlangıç ​​noktasının derleyicinin ürettiği noktaya yakın olduğundan emin olmak yani hiçbir şey kötü varsayımlarla optimize edilmez). İkincisi ilk olarak başka bir cevapta kullanıldı ve o sırada kendi bilgimde birkaç boşluk vardı, bu yüzden kodu el ile derleyerek bu cevabı verirken bunları doldurmaktan kendim yararlandım.
Jon Hanna

13

Burada zaten bir sürü harika cevap var; Zihinsel bir model oluşturmaya yardımcı olabilecek birkaç bakış açısını paylaşacağım.

İlk olarak, bir asyncyöntem derleyici tarafından birkaç parçaya bölünür; awaitifadeleri kırılma noktalarıdır. (Bu, basit yöntemler için kolayca anlaşılabilir; döngüleri olan daha karmaşık yöntemler ve istisna işleme, daha karmaşık bir durum makinesinin eklenmesiyle bozulur).

İkincisi, awaitoldukça basit bir diziye çevrilir; Lucian'ın "eğer beklenebilir zaten tamamlanmışsa, sonucu alın ve bu yöntemi uygulamaya devam edin; aksi takdirde, bu yöntemin durumunu kaydedin ve geri dönün" olan açıklamasını seviyorum . ( asyncGiriş bölümümde çok benzer bir terminoloji kullanıyorum ).

Bir bekleme süresine ulaşıldığında, çalışma zamanı bundan sonra hangi kod parçasının çalıştırılması gerektiğini nasıl bilir?

Yöntemin geri kalanı, bu beklenebilir için bir geri arama olarak mevcuttur (görevler durumunda, bu geri aramalar devamlardır). Beklenebilir tamamlandığında, geri aramalarını çağırır.

Çağrı yığını olduğunu unutmayın değil kaydedilir ve restore; geri aramalar doğrudan çağrılır. Çakışan G / Ç durumunda, bunlar doğrudan iş parçacığı havuzundan çağrılır.

Bu geri aramalar, yöntemi doğrudan yürütmeye devam edebilir veya başka bir yerde çalışacak şekilde programlayabilir (örn. await yakalanan bir UI SynchronizationContextve iş parçacığı havuzunda G / Ç tamamlandığında).

Ne zaman kaldığı yerden devam edebileceğini nasıl biliyor ve nerede olduğunu nasıl hatırlıyor?

Hepsi sadece geri aramalar. Beklenebilir bir tamamlandığında, geri aramalarını ve herhangi birasync önceden yapılmış olan yöntem awaitdevam . Geri çağırma, bu yöntemin ortasına atlar ve yerel değişkenleri kapsamda bulunur.

Geri aramaları edilir değil , belirli bir iş parçacığı çalıştırın ve yaptıkları değil onların callstack restore var.

Mevcut çağrı yığınına ne oluyor, bir şekilde kaydediliyor mu? Ya arama yöntemi beklemeden önce başka yöntem çağrıları yaparsa - neden yığının üzerine yazılmaz? Ve bir istisna ve yığın çözülme durumunda çalışma zamanı tüm bunları nasıl halledebilir?

Çağrı yığını ilk etapta kaydedilmez; gerekli değil.

Eşzamanlı kodla, tüm arayanlarınızı içeren bir çağrı yığını elde edebilirsiniz ve çalışma zamanı bunu kullanarak nereye döneceğini bilir.

Eşzamansız kodla, bir grup geri arama işaretçisi elde edebilirsiniz - görevini tamamlayan bazı G / Ç işlemlerinde kök salmış ve bir async yöntemi , görevini bitiren bir asyncyöntemi devam ettirebilir , vb.

Bu nedenle, eşzamanlı kod Aarama Bçağrısı ile Cçağrı yığınınız şöyle görünebilir:

A:B:C

eşzamansız kod geri aramaları (işaretçiler) kullanırken:

A <- B <- C <- (I/O operation)

Verime ulaşıldığında, çalışma zamanı eşyaların nereden alınması gerektiğini nasıl takip ediyor? Yineleyici durumu nasıl korunur?

Şu anda, oldukça verimsiz. :)

Diğer lambda gibi çalışır - değişken yaşam süreleri uzatılır ve referanslar yığın üzerinde yaşayan bir durum nesnesine yerleştirilir. Tüm derin seviye ayrıntılar için en iyi kaynak Jon Skeet'in EduAsync serisidir .


7

yield ve await her ikisi de akış kontrolüyle uğraşırken, tamamen farklı iki şeydir. Bu yüzden onları ayrı ayrı ele alacağım.

Amacı, yieldtembel diziler oluşturmayı kolaylaştırmaktır. yieldİçinde bir ifade bulunan bir numaralandırıcı döngüsü yazdığınızda, derleyici görmediğiniz bir ton yeni kod üretir. Kaputun altında, aslında yepyeni bir sınıf yaratır. Sınıf, döngünün durumunu izleyen üyeler ve IEnumerable uygulamasının bir uygulamasını içerir, böylece onu her çağırdığınızda MoveNextbu döngü boyunca bir kez daha adımlar atılır. Yani böyle bir foreach döngüsü yaptığınızda:

foreach(var item in mything.items()) {
    dosomething(item);
}

oluşturulan kod şuna benzer:

var i = mything.items();
while(i.MoveNext()) {
    dosomething(i.Current);
}

Mything.items () uygulamasının içinde, döngünün bir "adımını" gerçekleştirip geri dönen bir grup durum makinesi kodu vardır. Yani bunu kaynakta basit bir döngü gibi yazarken, başlık altında bu basit bir döngü değil. Yani derleyici hilesi. Kendinizi görmek istiyorsanız, ILDASM veya ILSpy veya benzer araçları çıkarın ve oluşturulan IL'nin neye benzediğini görün. Öğretici olmalı.

asyncve awaitdiğer yandan, bambaşka bir su ısıtıcısı balık. Await, soyut olarak, bir senkronizasyon ilkelidir. Sisteme "Bu tamamlanana kadar devam edemem" demenin bir yolu. Ancak, belirttiğiniz gibi, her zaman söz konusu olan bir konu yoktur.

Ne olduğunu dahil bir senkronizasyon bağlam denir şeydir. Her zaman etrafta dolanan biri vardır. Senkronizasyon bağlamının işi, beklenen görevleri ve devamlarını planlamaktır.

Söylediğinizde await thisThing()birkaç şey olur. Zaman uyumsuz bir yöntemde, derleyici aslında yöntemi daha küçük parçalara böler, her parça bir "bekleme öncesi" bölümü ve "bekleme sonrası" (veya devam) bölümüdür. Bekleme yürütüldüğünde, beklenen görev ve sonraki devam - başka bir deyişle, işlevin geri kalanı - senkronizasyon bağlamına geçirilir. Bağlam, görevin planlanmasıyla ilgilenir ve tamamlandığında bağlam, istediği dönüş değerini geçerek devamı çalıştırır.

Senkronizasyon bağlamı, işleri planladığı sürece istediği her şeyi yapmakta serbesttir. İş parçacığı havuzunu kullanabilir. Görev başına bir iş parçacığı oluşturabilir. Bunları senkronize olarak çalıştırabilir. Farklı ortamlar (ASP.NET ve WPF), ortamları için en iyi olanı temel alarak farklı şeyler yapan farklı eşitleme bağlamı uygulamaları sağlar.

(Bonus: ne olduğunu hiç merak ettim .ConfigurateAwait(false) mi? Sisteme geçerli eşitleme bağlamını kullanmamasını (genellikle proje türünüze göre - örneğin WPF'ye karşı ASP.NET) ve bunun yerine iş parçacığı havuzunu kullanan varsayılanı kullanmasını söylüyor.

Yani yine, bu bir çok derleyici hilesi. Oluşturulan koda bakarsanız karmaşıktır, ancak ne yaptığını görebilmeniz gerekir. Bu tür dönüşümler zordur, ancak belirleyici ve matematikseldir, bu yüzden derleyicinin bunları bizim için yapması harikadır.

PS Varsayılan senkronizasyon bağlamlarının varlığının bir istisnası vardır - konsol uygulamaları varsayılan bir senkronizasyon bağlamına sahip değildir. Kontrol Stephen Toub blog çok daha fazla bilgi için bkz. Hakkında asyncve awaitgenel olarak bilgi aramak için harika bir yer .


1
"Sisteme varsayılan senkronizasyon bağlamını kullanmamasını ve bunun yerine iş parçacığı havuzunu kullanan varsayılanı kullanmasını söylüyor" bununla ne demek istediğinizi açıklayabilir misiniz? "varsayılanı kullanma, varsayılanı kullan"
Kroltan

3
Pardon, terminolojimi karıştırdım, yazıyı düzelteceğim. Temel olarak, içinde bulunduğunuz ortam için varsayılanı kullanmayın, .NET için varsayılan olanı kullanın (yani iş parçacığı havuzu).
Chris Tavares

çok basit, anlayabildim, oyumu aldınız :)
Ehsan Sajjad

4

Normalde, CIL'e bakmanızı öneririm, ancak bu durumda, bu bir karmaşa.

Bu iki dil yapısı çalışma açısından benzerdir, ancak biraz farklı şekilde uygulanır. Temel olarak, bu sadece bir derleyici sihri için sözdizimsel bir şekerdir, montaj seviyesinde çılgın / güvensiz hiçbir şey yoktur. Bunlara kısaca bakalım.

yielddaha eski ve daha basit bir ifadedir ve temel durum makinesi için sözdizimsel bir şekerdir. Bir yöntem olup dönen IEnumerable<T>ya da IEnumerator<T>bir içerebilir yieldsonra durum makinesi fabrikasına yöntemi dönüşümleri. Dikkat etmeniz gereken bir şey, yöntemde herhangi bir kodun, onu çağırdığınızda, yieldiçeride varsa, çalıştırılmamasıdır . Bunun nedeni, yazdığınız kodun IEnumerator<T>.MoveNext, içinde bulunduğu durumu kontrol eden ve kodun doğru kısmını çalıştıran yönteme taşınmasıdır . yield return x;daha sonra benzer bir şeye dönüştürülürthis.Current = x; return true;

Biraz düşünürseniz, inşa edilmiş durum makinesini ve alanlarını kolayca inceleyebilirsiniz (en az bir tane eyalet ve yerel halk için). Alanları değiştirseniz bile sıfırlayabilirsiniz.

awaittür kitaplığından biraz destek gerektirir ve biraz farklı çalışır. Bir Taskveya Task<T>bağımsız değişken alır , sonra görev tamamlandığında değerine ulaşır veya yoluyla bir devam kaydeder Task.GetAwaiter().OnCompleted. async/ awaitSisteminin tam olarak uygulanmasının açıklanması çok uzun sürer, ancak bu o kadar mistik de değildir. Aynı zamanda bir durum makinesi yaratır ve onu devamı boyunca OnCompleted'e iletir . Görev tamamlanırsa, sonucunu devamında kullanır. Bekleyenin uygulanması, devamın nasıl başlatılacağına karar verir. Genellikle çağıran iş parçacığının senkronizasyon bağlamını kullanır.

Hem yieldveawait bir durum makinesi oluşturmak için yöntemi, oluşma durumuna göre bölmek zorundadır; makinenin her bir dalı, yöntemin her bir parçasını temsil eder.

Bu kavramları yığınlar, iplikler vb. Gibi "alt düzey" terimlerle düşünmemelisiniz. Bunlar soyutlamalardır ve içsel çalışmaları CLR'den herhangi bir destek gerektirmez, sihri yapan sadece derleyicidir. Bu çalışma zamanının destek var Lua en değiş tokuş eden kavramlar, ya C'nin gelen çılgınca farklı longjmp sadece kara büyü olduğunu.


5
Yan not : Görevawait almak zorunda değildir . İle herhangi bir şey yeterlidir. İhtiyaç olmadığına biraz benzer , herhangi bir şey yeterlidir. INotifyCompletion GetAwaiter()foreachIEnumerableIEnumerator GetEnumerator()
IllidanS4 Monica'yı
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.