Bagaimana hasil dan menunggu menerapkan aliran kontrol di .NET?

105

Seperti yang saya pahami yieldkata kunci, jika digunakan dari dalam blok iterator, ia mengembalikan aliran kontrol ke kode panggilan, dan ketika iterator dipanggil lagi, ia mengambil di mana ia tinggalkan.

Juga, awaittidak hanya menunggu callee, tetapi mengembalikan kontrol ke pemanggil, hanya untuk mengambil di mana ia tinggalkan saat pemanggil awaitsmetode.

Dengan kata lain-- tidak ada utas , dan "konkurensi" async dan menunggu adalah ilusi yang disebabkan oleh aliran kontrol yang cerdas, detailnya disembunyikan oleh sintaksis.

Sekarang, saya adalah mantan programmer perakitan dan saya sangat akrab dengan petunjuk instruksi, tumpukan, dll. Dan saya mendapatkan cara kerja normal aliran kontrol (subrutin, rekursi, loop, cabang). Tapi konstruksi baru ini-- Saya tidak mengerti.

Ketika sebuah awaittercapai, bagaimana runtime mengetahui bagian kode apa yang harus dieksekusi selanjutnya? Bagaimana cara mengetahui kapan dapat dilanjutkan dari tempat yang ditinggalkannya, dan bagaimana ia mengingat di mana? Apa yang terjadi pada tumpukan panggilan saat ini, apakah itu disimpan entah bagaimana? Bagaimana jika metode panggilan membuat panggilan metode lain sebelum itu - awaitmengapa tumpukan tidak ditimpa? Dan bagaimana mungkin runtime bekerja melalui semua ini dalam kasus pengecualian dan tumpukan terlepas?

Ketika yieldtercapai, bagaimana runtime melacak titik di mana hal-hal harus diambil? Bagaimana status iterator dipertahankan?

John Wu
sumber
4
Anda dapat melihat di kode yang dihasilkan dalam TryRoslyn compiler secara online
xanatos
1
Anda mungkin ingin memeriksa seri artikel Eduasync oleh Jon Skeet.
Leonid Vasilev
Bacaan menarik terkait: stackoverflow.com/questions/37419572/…
Jason C

Jawaban:

115

Saya akan menjawab pertanyaan spesifik Anda di bawah ini, tetapi sebaiknya Anda membaca artikel ekstensif saya tentang bagaimana kami merancang hasil dan menunggu.

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/

Beberapa dari artikel ini sudah usang sekarang; kode yang dihasilkan berbeda dalam banyak hal. Tapi ini pasti akan memberi Anda gambaran tentang cara kerjanya.

Juga, jika Anda tidak memahami bagaimana lambda dibuat sebagai kelas penutup, pahami itu terlebih dahulu . Anda tidak akan membuat kepala atau ekor menjadi asinkron jika Anda tidak memiliki lambda ke bawah.

Saat menunggu tercapai, bagaimana runtime mengetahui bagian kode apa yang harus dieksekusi selanjutnya?

await dihasilkan sebagai:

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

Itu pada dasarnya. Menunggu hanyalah pengembalian yang mewah.

Bagaimana cara mengetahui kapan dapat dilanjutkan dari tempat yang ditinggalkannya, dan bagaimana ia mengingat di mana?

Nah, bagaimana Anda melakukannya tanpa menunggu? Ketika metode foo memanggil bilah metode, entah bagaimana kami ingat bagaimana kembali ke tengah-tengah foo, dengan semua penduduk lokal aktivasi foo utuh, tidak peduli bilah apa yang dilakukannya.

Anda tahu bagaimana hal itu dilakukan di assembler. Catatan aktivasi untuk foo didorong ke stack; itu mengandung nilai-nilai penduduk setempat. Pada titik panggilan, alamat pengirim di foo didorong ke stack. Ketika bilah selesai, penunjuk tumpukan dan penunjuk instruksi disetel ulang ke tempat yang mereka butuhkan dan foo terus berjalan dari tempat terakhirnya.

Kelanjutan dari menunggu persis sama, kecuali bahwa record diletakkan di heap karena alasan yang jelas bahwa urutan aktivasi tidak membentuk stack .

Delegasi yang menunggu diberikan sebagai kelanjutan dari tugas berisi (1) angka yang merupakan masukan ke tabel pencarian yang memberikan penunjuk instruksi yang perlu Anda jalankan selanjutnya, dan (2) semua nilai lokal dan temporari.

Ada beberapa perlengkapan tambahan di sana; misalnya, di .NET adalah ilegal untuk bercabang di tengah blok percobaan, jadi Anda tidak bisa begitu saja memasukkan alamat kode di dalam blok percobaan ke dalam tabel. Tapi ini adalah detail pembukuan. Secara konseptual, catatan aktivasi hanya dipindahkan ke heap.

Apa yang terjadi pada tumpukan panggilan saat ini, apakah itu disimpan entah bagaimana?

Informasi yang relevan dalam catatan aktivasi saat ini tidak pernah diletakkan di tumpukan sejak awal; itu dialokasikan dari heap sejak awal. (Nah, parameter formal diteruskan di stack atau di register secara normal dan kemudian disalin ke lokasi heap saat metode dimulai.)

Catatan aktivasi penelepon tidak disimpan; penantian mungkin akan kembali kepada mereka, ingat, jadi mereka akan ditangani secara normal.

Perhatikan bahwa ini adalah perbedaan erat antara gaya penerusan lanjutan yang disederhanakan dari menunggu, dan struktur panggilan-dengan-kelanjutan yang sebenarnya yang Anda lihat dalam bahasa seperti Skema. Dalam bahasa-bahasa tersebut seluruh kelanjutan termasuk kelanjutan kembali ke pemanggil ditangkap oleh panggilan-cc .

Bagaimana jika metode panggilan membuat panggilan metode lain sebelum menunggu-- mengapa tumpukan tidak ditimpa?

Panggilan metode tersebut kembali, sehingga catatan aktivasi mereka tidak lagi berada di tumpukan pada titik menunggu.

Dan bagaimana mungkin runtime bekerja melalui semua ini dalam kasus pengecualian dan tumpukan terlepas?

Jika terjadi pengecualian yang tidak tertangkap, pengecualian tersebut ditangkap, disimpan di dalam tugas, dan ditampilkan kembali saat hasil tugas diambil.

Ingat semua pembukuan yang saya sebutkan sebelumnya? Mendapatkan pengecualian semantik dengan benar adalah rasa sakit yang luar biasa, izinkan saya memberi tahu Anda.

Ketika hasil tercapai, bagaimana runtime melacak titik di mana hal-hal harus diambil? Bagaimana status iterator dipertahankan?

Cara yang sama. Status penduduk setempat dipindahkan ke heap, dan nomor yang mewakili instruksi yang MoveNextharus dilanjutkan saat dipanggil berikutnya disimpan bersama dengan penduduk setempat.

Dan lagi, ada banyak roda gigi di blok iterator untuk memastikan bahwa pengecualian ditangani dengan benar.

Eric Lippert
sumber
1
karena latar belakang penulis pertanyaan (assembler et al), mungkin perlu disebutkan bahwa kedua konstruksi ini tidak mungkin dilakukan tanpa memori terkelola. tanpa memori yang dikelola mencoba untuk mengkoordinasikan masa penutupan pasti akan membuat Anda tersandung pada bootstraps Anda.
Jim
Link semua halaman tidak ditemukan (404)
Digital3D
Semua artikel Anda tidak tersedia sekarang. Bisakah Anda memposting ulang mereka?
Michał Turczyn
1
@ MichałTurczyn: Mereka masih di internet; Microsoft terus berpindah ke tempat arsip blog berada. Saya akan memindahkan semuanya secara bertahap ke situs pribadi saya, dan mencoba memperbarui tautan ini ketika saya punya waktu.
Eric Lippert
38

yield lebih mudah dari keduanya, jadi mari kita periksa.

Katakanlah kita punya:

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

Ini akan dikompilasi sedikit seperti jika kita menulis:

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

Jadi, tidak seefisien implementasi tulisan tangan IEnumerable<int>dan IEnumerator<int>(misalnya kita tidak akan menyia-nyiakan memiliki yang terpisah _state, _idan _currentdalam hal ini) tetapi tidak buruk (trik menggunakan kembali sendiri ketika aman untuk melakukannya daripada membuat yang baru object is good), dan dapat dikembangkan untuk menangani yieldmetode -menggunakan yang sangat rumit .

Dan tentu saja sejak itu

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

Sama dengan:

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

Kemudian yang dihasilkan MoveNext()dipanggil berulang kali.

The asynckasus ini cukup banyak prinsip yang sama, tetapi dengan sedikit kompleksitas ekstra. Untuk menggunakan kembali contoh dari kode jawaban lain seperti:

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

Menghasilkan kode seperti:

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

Ini lebih rumit, tetapi prinsip dasarnya sangat mirip. Komplikasi tambahan utama adalah yang sekarang GetAwaiter()digunakan. Jika suatu saat awaiter.IsCompleteddicentang, ia kembali truekarena tugas awaitsudah selesai (misalnya kasus di mana ia bisa kembali secara sinkron) maka metode akan terus bergerak melalui status, tetapi sebaliknya ia menetapkan dirinya sendiri sebagai panggilan balik ke awaiter.

Apa yang terjadi dengan itu tergantung pada awaiter, dalam hal apa yang memicu callback (misalnya penyelesaian I / O asinkron, tugas yang berjalan pada penyelesaian thread) dan persyaratan apa yang ada untuk marshalling ke utas tertentu atau berjalan di utas threadpool , konteks apa dari panggilan asli yang mungkin diperlukan atau tidak diperlukan dan seterusnya. Apapun itu meskipun sesuatu dalam penunggu itu akan memanggil MoveNextdan itu akan melanjutkan dengan bagian pekerjaan berikutnya (hingga berikutnya await) atau selesai dan kembali dalam hal Taskyang sedang dilaksanakan menjadi selesai.

Jon Hanna
sumber
Apakah Anda meluangkan waktu untuk menggulung terjemahan Anda sendiri? O_O uao.
CoffeDeveloper
4
@DarioOO Hal pertama yang dapat saya lakukan dengan cukup cepat, telah melakukan banyak terjemahan dari yieldke manual ketika ada keuntungan dalam melakukannya (umumnya sebagai pengoptimalan, tetapi ingin memastikan bahwa titik awalnya dekat dengan yang dihasilkan kompiler jadi tidak ada yang menjadi tidak dioptimalkan melalui asumsi buruk). Yang kedua pertama kali digunakan dalam jawaban lain dan ada beberapa celah dalam pengetahuan saya pada saat itu, jadi saya mendapat manfaat dari mengisi jawaban tersebut sambil memberikan jawaban itu dengan menguraikan kode secara manual.
Jon Hanna
13

Ada banyak sekali jawaban bagus di sini; Saya hanya akan membagikan beberapa sudut pandang yang dapat membantu membentuk model mental.

Pertama, asyncmetode dipecah menjadi beberapa bagian oleh kompilator; yang awaitekspresi adalah poin fraktur. (Ini mudah dipahami untuk metode sederhana; metode yang lebih kompleks dengan loop dan penanganan pengecualian juga terputus, dengan penambahan mesin status yang lebih kompleks).

Kedua, awaitditerjemahkan ke dalam urutan yang cukup sederhana; Saya suka deskripsi Lucian , yang dengan kata lain cukup banyak "jika menunggu sudah selesai, dapatkan hasilnya dan lanjutkan menjalankan metode ini; jika tidak, simpan status metode ini dan kembalikan". (Saya menggunakan terminologi yang sangat mirip di asyncintro saya ).

Saat menunggu tercapai, bagaimana runtime mengetahui bagian kode apa yang harus dieksekusi selanjutnya?

Sisa dari metode ini ada sebagai callback untuk yang menunggu (dalam kasus tugas, callback ini adalah kelanjutan). Saat awaitable selesai, ia memanggil callback-nya.

Perhatikan bahwa tumpukan panggilan tidak disimpan dan dipulihkan; callback dipanggil secara langsung. Dalam kasus I / O yang tumpang tindih, mereka dipanggil langsung dari kumpulan thread.

Callback tersebut dapat terus mengeksekusi metode secara langsung, atau menjadwalkannya untuk dijalankan di tempat lain (misalnya, jika awaitUI SynchronizationContextdan I / O yang diambil selesai di kumpulan thread).

Bagaimana cara mengetahui kapan dapat dilanjutkan dari tempat yang ditinggalkannya, dan bagaimana ia mengingat di mana?

Itu semua hanya panggilan balik. Saat awaitable selesai, ia memanggil callback-nya, dan apa sajaasync metode yang telah awaitmengubahnya akan dilanjutkan. Callback melompat ke tengah metode itu dan memiliki variabel lokalnya dalam cakupan.

Callback tidak menjalankan utas tertentu, dan tidak memiliki callstack mereka dipulihkan.

Apa yang terjadi pada tumpukan panggilan saat ini, apakah itu disimpan entah bagaimana? Bagaimana jika metode panggilan membuat panggilan metode lain sebelum menunggu-- mengapa tumpukan tidak ditimpa? Dan bagaimana mungkin runtime bekerja melalui semua ini dalam kasus pengecualian dan tumpukan terlepas?

Callstack tidak disimpan sejak awal; itu tidak perlu.

Dengan kode sinkron, Anda bisa mendapatkan tumpukan panggilan yang menyertakan semua pemanggil Anda, dan runtime tahu ke mana harus kembali menggunakannya.

Dengan kode asinkron, Anda dapat berakhir dengan sekumpulan penunjuk panggilan balik - berakar pada beberapa operasi I / O yang menyelesaikan tugasnya, yang dapat melanjutkan asyncmetode yang menyelesaikan tugasnya, yang dapat melanjutkan asyncmetode yang menyelesaikan tugasnya, dll.

Jadi, dengan sinkron kode Apanggilan Bpanggilan C, callstack Anda mungkin terlihat seperti ini:

A:B:C

sedangkan kode asynchronous menggunakan callback (pointer):

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

Ketika hasil tercapai, bagaimana runtime melacak titik di mana hal-hal harus diambil? Bagaimana status iterator dipertahankan?

Saat ini, agak tidak efisien. :)

Ia bekerja seperti lambda lainnya - masa pakai variabel diperpanjang dan referensi ditempatkan ke dalam objek status yang hidup di tumpukan. Sumber daya terbaik untuk semua detail tingkat dalam adalah seri EduAsync dari Jon Skeet .

Stephen Cleary
sumber
7

yielddan await, meskipun keduanya berurusan dengan kontrol aliran, dua hal yang sama sekali berbeda. Jadi saya akan menangani mereka secara terpisah.

Tujuannya yieldadalah untuk mempermudah pembuatan urutan pemalas. Saat Anda menulis enumerator-loop dengan yieldpernyataan di dalamnya, kompilator menghasilkan banyak kode baru yang tidak Anda lihat. Di bawah tenda, itu benar-benar menghasilkan kelas yang sama sekali baru. Kelas berisi anggota yang melacak status loop, dan implementasi IEnumerable sehingga setiap kali Anda memanggilnya MoveNext, langkah-langkah sekali lagi melalui loop itu. Jadi saat Anda melakukan foreach loop seperti ini:

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

kode yang dihasilkan terlihat seperti ini:

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

Di dalam implementasi mything.items () adalah sekumpulan kode mesin negara yang akan melakukan satu "langkah" dari loop kemudian kembali. Jadi, saat Anda menulisnya di sumber seperti loop sederhana, di balik terpal itu bukan loop sederhana. Jadi tipu muslihat kompiler. Jika Anda ingin melihat diri Anda sendiri, tarik ILDASM atau ILSpy atau alat serupa dan lihat seperti apa IL yang dihasilkan. Itu harus instruktif.

asyncdan await, di sisi lain, adalah ketel ikan lainnya. Await adalah, secara abstrak, primitif sinkronisasi. Ini adalah cara untuk memberi tahu sistem "Saya tidak bisa melanjutkan sampai ini selesai." Tapi, seperti yang Anda catat, tidak selalu ada utas yang terlibat.

Apa yang terlibat adalah sesuatu yang disebut konteks sinkronisasi. Selalu ada orang yang berkeliaran. Pekerjaan konteks sinkronisasi mereka adalah menjadwalkan tugas yang sedang ditunggu dan kelanjutannya.

Saat Anda mengatakan await thisThing(), beberapa hal terjadi. Dalam metode async, kompilator benar-benar memotong metode menjadi potongan yang lebih kecil, setiap potongan menjadi bagian "sebelum menunggu" dan bagian "setelah menunggu" (atau lanjutan). Saat await dijalankan, tugas sedang ditunggu, dan kelanjutan berikut - dengan kata lain, fungsi lainnya - diteruskan ke konteks sinkronisasi. Konteks menangani penjadwalan tugas, dan ketika selesai konteks kemudian menjalankan kelanjutan, meneruskan nilai kembali apa pun yang diinginkannya.

Konteks sinkronisasi bebas untuk melakukan apa pun yang diinginkan selama itu menjadwalkan hal-hal. Itu bisa menggunakan kumpulan benang. Itu bisa membuat utas per tugas. Itu bisa menjalankannya secara sinkron. Lingkungan yang berbeda (ASP.NET vs. WPF) menyediakan implementasi konteks sinkronisasi berbeda yang melakukan hal berbeda berdasarkan apa yang terbaik untuk lingkungannya.

(Bonus: pernah bertanya-tanya apa .ConfigurateAwait(false)fungsinya? Ini memberi tahu sistem untuk tidak menggunakan konteks sinkronisasi saat ini (biasanya berdasarkan jenis proyek Anda - WPF vs ASP.NET misalnya) dan sebagai gantinya gunakan yang default, yang menggunakan kumpulan utas).

Jadi sekali lagi, ini banyak tipuan kompiler. Jika Anda melihat kode yang dihasilkan itu rumit tetapi Anda harus dapat melihat apa yang dilakukannya. Jenis transformasi ini sulit, tetapi deterministik dan matematis, itulah mengapa sangat bagus bahwa kompiler melakukannya untuk kita.

PS Ada satu pengecualian untuk keberadaan konteks sinkronisasi default - aplikasi konsol tidak memiliki konteks sinkronisasi default. Periksa blog Stephen Toub untuk informasi lebih lanjut. Ini adalah tempat yang tepat untuk mencari informasi asyncdan awaitsecara umum.

Chris Tavares
sumber
1
"Ini memberitahu sistem untuk tidak menggunakan konteks sinkronisasi default dan sebagai gantinya menggunakan yang default, yang menggunakan kumpulan utas" dapatkah Anda menjelaskan apa yang Anda maksud dengan ini? "jangan gunakan default, gunakan default"
Kroltan
3
Maaf, terminologi saya salah, saya akan perbaiki postingannya. Pada dasarnya, jangan gunakan yang default untuk lingkungan tempat Anda berada, gunakan yang default untuk .NET (yaitu kumpulan thread).
Chris Tavares
sangat sederhana, bisa dipahami, Anda mendapat suara saya :)
Ehsan Sajjad
4

Biasanya, saya akan merekomendasikan untuk melihat CIL, tetapi dalam kasus ini, itu berantakan.

Kedua konstruksi bahasa ini serupa dalam bekerja, tetapi diterapkan sedikit berbeda. Pada dasarnya, ini hanya gula sintaksis untuk sihir kompiler, tidak ada yang gila / tidak aman di tingkat perakitan. Mari kita lihat secara singkat.

yieldadalah pernyataan yang lebih lama dan lebih sederhana, dan ini adalah gula sintaksis untuk mesin status dasar. Sebuah metode yang kembali IEnumerable<T>atau IEnumerator<T>mungkin berisi yield, yang kemudian mengubah metode tersebut menjadi pabrik mesin keadaan. Satu hal yang harus Anda perhatikan adalah bahwa tidak ada kode dalam metode ini yang dijalankan saat Anda memanggilnya, jika ada yielddi dalamnya. Alasannya adalah bahwa kode yang Anda tulis dipindahkan ke IEnumerator<T>.MoveNextmetode, yang memeriksa statusnya dan menjalankan bagian kode yang benar. yield return x;kemudian diubah menjadi sesuatu yang mirip denganthis.Current = x; return true;

Jika Anda melakukan refleksi, Anda dapat dengan mudah memeriksa mesin negara bagian yang dibangun dan bidangnya (setidaknya satu untuk negara bagian dan untuk penduduk setempat). Anda bahkan dapat mengatur ulang jika Anda mengubah bidang.

awaitmembutuhkan sedikit dukungan dari pustaka tipe, dan bekerja agak berbeda. Dibutuhkan argumen Taskatau Task<T>, lalu hasilnya sesuai nilainya jika tugas selesai, atau mendaftarkan kelanjutan melalui Task.GetAwaiter().OnCompleted. Penerapan async/ awaitsystem secara penuh akan memakan waktu terlalu lama untuk dijelaskan, tetapi juga tidak terlalu mistis. Ini juga membuat mesin status dan meneruskannya di sepanjang lanjutan ke OnCompleted . Jika tugas selesai, itu kemudian menggunakan hasilnya di kelanjutan. Pelaksanaan awaiter memutuskan bagaimana meminta kelanjutan. Biasanya ini menggunakan konteks sinkronisasi utas panggilan.

Keduanya yielddan awaitharus membagi metode berdasarkan kemunculannya untuk membentuk mesin status, dengan setiap cabang mesin mewakili setiap bagian dari metode.

Anda tidak boleh memikirkan konsep ini dalam istilah "tingkat bawah" seperti tumpukan, utas, dll. Ini adalah abstraksi, dan cara kerja dalamnya tidak memerlukan dukungan apa pun dari CLR, hanya kompiler yang melakukan keajaiban. Ini sangat berbeda dari coroutine Lua, yang memiliki dukungan runtime, atau longjmp C , yang hanya sihir hitam.

IllidanS4 ingin Monica kembali
sumber
5
Catatan samping : awaittidak harus mengambil Tugas . Apa pun dengan itu INotifyCompletion GetAwaiter()sudah cukup. Agak mirip dengan bagaimana foreachtidak perlu IEnumerable, apapun dengan IEnumerator GetEnumerator()sudah cukup.
IllidanS4 ingin Monica kembali