Apakah struktur tumpukan digunakan untuk proses async?


10

Pertanyaan ini memiliki jawaban yang sangat bagus oleh Eric Lippert yang menjelaskan untuk apa tumpukan itu digunakan. Untuk tahun ini saya sudah tahu - secara umum - apa tumpukan itu dan bagaimana itu digunakan, tetapi bagian dari jawabannya membuat saya bertanya-tanya apakah struktur tumpukan ini kurang digunakan hari ini di mana pemrograman async adalah norma.

Dari jawabannya:

tumpukan adalah bagian dari reifikasi kelanjutan dalam bahasa tanpa coroutine.

Secara khusus, bagian tanpa coroutine ini membuat saya bertanya-tanya.

Dia menjelaskan lebih banyak di sini:

Coroutine adalah fungsi yang dapat mengingat di mana mereka berada, memberikan kontrol ke coroutine lain untuk sementara waktu, dan kemudian melanjutkan di mana mereka tinggalkan nanti, tetapi tidak harus segera setelah hasil coroutine yang baru saja disebut. Pikirkan "hasil pengembalian" atau "menunggu" di C #, yang harus mengingat di mana mereka berada ketika item berikutnya diminta atau operasi asinkron selesai. Bahasa dengan coroutine atau fitur bahasa yang serupa membutuhkan struktur data yang lebih maju daripada tumpukan untuk mengimplementasikan kelanjutan.

Ini sangat bagus dalam hal stack, tetapi meninggalkan saya dengan pertanyaan yang belum terjawab tentang struktur apa yang digunakan ketika stack terlalu sederhana untuk menangani fitur bahasa ini yang membutuhkan struktur data yang lebih maju?

Apakah tumpukan hilang seiring kemajuan teknologi? Apa yang menggantikannya? Apakah ini jenis hibrida? (misalnya, apakah program .NET saya menggunakan stack sampai hit async call kemudian beralih ke beberapa struktur lain sampai selesai, di mana stack dibatalkan kembali ke keadaan di mana ia dapat memastikan item berikutnya, dll? )

Bahwa skenario ini terlalu maju untuk stack masuk akal, tetapi apa yang menggantikan stack? Ketika saya telah belajar tentang ini tahun lalu, tumpukan itu ada di sana karena kilat cepat dan ringan, sepotong memori yang dialokasikan pada aplikasi jauh dari tumpukan karena mendukung manajemen yang sangat efisien untuk tugas yang sedang dikerjakan (pun intended?). Apa yang berubah?

Jawaban:


14

Apakah ini jenis hibrida? (misalnya, apakah program .NET saya menggunakan stack sampai hit async call kemudian beralih ke beberapa struktur lain sampai selesai, di mana stack dibatalkan kembali ke keadaan di mana ia dapat memastikan item berikutnya, dll? )

Pada dasarnya ya.

Misalkan kita punya

async void MyButton_OnClick() { await Foo(); Bar(); }
async Task Foo() { await Task.Delay(123); Blah(); }

Berikut adalah penjelasan yang sangat disederhanakan tentang bagaimana kelanjutannya diverifikasi. Kode yang sebenarnya jauh lebih kompleks, tetapi hal ini membuat gagasan tersebut melintas.

Anda mengklik tombol. Sebuah pesan diantrekan. Putaran pesan memproses pesan dan memanggil penangan klik, meletakkan alamat pengirim antrian pesan di tumpukan. Artinya, hal yang terjadi setelah handler selesai adalah bahwa loop pesan harus tetap berjalan. Jadi kelanjutan dari handler adalah loop.

Pawang klik memanggil Foo (), meletakkan alamat pengirim itu sendiri di tumpukan. Artinya, kelanjutan dari Foo adalah sisa dari pengendali klik.

Foo memanggil Task.Delay, meletakkan alamat pengirim itu sendiri di stack.

Task.Delay melakukan sihir apa pun yang perlu dilakukan untuk segera mengembalikan Tugas. Tumpukan muncul dan kami kembali ke Foo.

Foo memeriksa tugas yang dikembalikan untuk melihat apakah sudah selesai. Bukan itu. The kelanjutan dari Tunggulah, adalah untuk memanggil Blah (), sehingga Foo menciptakan delegasi yang panggilan Blah (), dan tanda-tanda yang mendelegasikan sebagai kelanjutan dari tugas. (Saya baru saja membuat pernyataan yang keliru; apakah Anda menangkapnya? Jika tidak, kami akan mengungkapkannya sebentar lagi.)

Foo kemudian membuat objek Tugasnya sendiri, menandainya sebagai tidak lengkap, dan mengembalikannya ke penangan klik.

Handler klik memeriksa tugas Foo dan menemukan itu tidak lengkap. Kelanjutan dari menunggu dalam handler adalah memanggil Bar (), jadi handler klik membuat delegasi yang memanggil Bar () dan menetapkannya sebagai kelanjutan dari tugas yang dikembalikan oleh Foo (). Kemudian mengembalikan tumpukan ke loop pesan.

Lingkaran pesan terus memproses pesan. Akhirnya sihir timer yang dibuat oleh tugas penundaan melakukan tugasnya dan memposting pesan ke antrian yang mengatakan bahwa kelanjutan tugas penundaan sekarang dapat dieksekusi. Jadi loop pesan memanggil kelanjutan tugas, menempatkan dirinya di tumpukan seperti biasa. Delegasi itu memanggil Blah (). Blah () melakukan apa yang dilakukannya dan mengembalikan tumpukan.

Sekarang apa yang terjadi? Inilah bagian yang sulit. Kelanjutan dari tugas penundaan tidak hanya memanggil Blah (). Itu juga harus memicu panggilan ke Bar () , tetapi tugas itu tidak tahu tentang Bar!

Foo benar-benar membuat delegasi yang (1) memanggil Blah (), dan (2) memanggil kelanjutan dari tugas yang Foo buat dan serahkan kembali ke pengendali acara. Begitulah cara kami memanggil delegasi yang memanggil Bar ().

Dan sekarang kami telah melakukan semua yang perlu kami lakukan, dalam urutan yang benar. Tapi kami tidak pernah berhenti memproses pesan dalam loop pesan terlalu lama, sehingga aplikasi tetap responsif.

Bahwa skenario ini terlalu maju untuk stack masuk akal, tetapi apa yang menggantikan stack?

Grafik objek tugas yang berisi referensi satu sama lain melalui kelas penutupan delegasi. Kelas-kelas penutupan adalah mesin negara yang melacak posisi menunggu yang paling baru dieksekusi dan nilai-nilai penduduk setempat. Plus, dalam contoh yang diberikan, antrian tindakan negara-global diimplementasikan oleh sistem operasi dan loop pesan yang mengeksekusi tindakan tersebut.

Latihan: bagaimana Anda mengira ini semua bekerja di dunia tanpa loop pesan? Misalnya, aplikasi konsol. menunggu di aplikasi konsol sangat berbeda; dapatkah Anda menyimpulkan cara kerjanya dari apa yang Anda ketahui sejauh ini?

Ketika saya telah belajar tentang ini tahun lalu, tumpukan itu ada di sana karena kilat cepat dan ringan, sepotong memori yang dialokasikan pada aplikasi jauh dari tumpukan karena mendukung manajemen yang sangat efisien untuk tugas yang sedang ditangani (pun intended?). Apa yang berubah?

Tumpukan adalah struktur data yang berguna ketika masa hidup aktivasi metode membentuk tumpukan, tetapi dalam contoh saya aktivasi penangan klik, Foo, Bar dan Blah tidak membentuk tumpukan. Dan oleh karena itu struktur data yang menyatakan bahwa alur kerja tidak bisa menjadi tumpukan; melainkan grafik tugas yang dialokasikan heap dan delegasi yang mewakili alur kerja. Menunggu adalah poin dalam alur kerja di mana kemajuan tidak dapat dibuat lebih lanjut dalam alur kerja sampai pekerjaan dimulai lebih awal telah selesai; sementara kita menunggu, kita dapat melakukan pekerjaan lain yang tidak bergantung pada tugas-tugas awal tertentu yang telah selesai.

Tumpukan hanyalah array dari frame, di mana frame berisi (1) pointer ke tengah fungsi (di mana panggilan terjadi) dan (2) nilai variabel lokal dan temps. Kelanjutan tugas adalah hal yang sama: delegasi adalah pointer ke fungsi dan memiliki keadaan yang merujuk titik tertentu di tengah fungsi (di mana menunggu terjadi), dan penutupan memiliki bidang untuk setiap variabel lokal atau sementara . Frame hanya tidak membentuk array yang bagus lagi, tetapi semua informasinya sama.


1
Terima kasih banyak. Jika saya dapat menandai kedua jawaban sebagai jawaban yang diterima, saya akan melakukannya, tetapi karena saya tidak bisa, saya akan membiarkannya kosong (tetapi, tidak ingin ada yang berpikir waktu untuk menjawab tidak dihargai)
jleach

3
@ jdl134679: Saya menyarankan Anda menandai sesuatu sebagai jawaban jika Anda merasa pertanyaan Anda telah dijawab; yang mengirimkan sinyal bahwa orang harus datang ke sini jika mereka ingin membaca jawaban yang baik daripada menulisnya . (Tentu saja menulis jawaban yang baik selalu dianjurkan.) Saya tidak peduli siapa yang mendapat tanda centang.
Eric Lippert

8

Appel menulis pengumpulan sampah kertas lama bisa lebih cepat dari alokasi tumpukan . Baca juga buku Kompilasi dengan kelanjutannya dan buku pegangan pengumpulan sampah . Beberapa teknik GC sangat efisien. The kelanjutan lewat gaya mendefinisikan kanonik seluruh program- transformasi (CPS transformasi ) untuk menyingkirkan tumpukan (konseptual mengganti frame panggilan dengan tumpukan-dialokasikan penutupan , dengan kata lain "reifying" frame panggilan individu sebagai "nilai-nilai" individu atau "objek" ).

Tetapi panggilan tumpukan masih sangat banyak digunakan, dan prosesor saat ini memiliki perangkat keras khusus (tumpukan register, mesin cache, ....) yang didedikasikan untuk tumpukan panggilan (dan itu karena sebagian besar bahasa pemrograman tingkat rendah, terutama C, lebih mudah untuk terapkan dengan tumpukan panggilan). Perhatikan juga bahwa tumpukan adalah cache yang ramah (dan yang penting sebuah banyak untuk kinerja).

Secara praktis, tumpukan panggilan masih ada di sini. Tetapi kami sekarang memiliki banyak dari mereka, dan kadang-kadang tumpukan panggilan dibagi dalam banyak segmen yang lebih kecil (misalnya masing-masing beberapa halaman dengan 4Kbytes), yang kadang-kadang dikumpulkan dengan sampah, atau dialokasikan dengan tumpukan. Segmen tumpukan ini dapat diatur dalam beberapa daftar tertaut (atau beberapa struktur data yang lebih rumit, jika diperlukan). Sebagai contoh, GCC compiler memiliki sebuah -fsplit-stackpilihan (terutama berguna untuk Go, dan "goroutines" dan "proses async"). Dengan tumpukan tumpukan, Anda dapat memiliki ribuan tumpukan (dan co-rutin menjadi lebih mudah diimplementasikan) yang terbuat dari jutaan segmen tumpukan kecil, dan "membuka gulungan" tumpukan mungkin lebih cepat (atau setidaknya hampir secepat secepat dengan satu potongan) tumpukan).

(dengan kata lain, perbedaan antara stack & heap kabur, tetapi mungkin memerlukan transformasi seluruh program, atau mengubah secara tidak kompatibel konvensi pemanggilan dan kompiler)

Lihat juga ini & itu dan banyak makalah (misalnya ini ) yang membahas transformasi CPS. Baca juga tentang ASLR & panggilan / cc . Baca (& STFW) lebih lanjut tentang kelanjutan .

Implementasi .CLR & .NET mungkin tidak memiliki transformasi GC & CPS yang canggih, karena banyak alasan pragmatis. Ini adalah trade-off terkait dengan transformasi seluruh program (dan kemudahan menggunakan rutinitas tingkat rendah C & memiliki runtime yang dikodekan dalam C atau C ++).

Chicken Scheme menggunakan tumpukan mesin (atau C) dengan cara yang tidak konvensional dengan transformasi CPS: setiap alokasi terjadi pada tumpukan, dan ketika itu menjadi terlalu besar, langkah penyalinan & penerusan GC generasional terjadi untuk memindahkan nilai alokasi tumpukan terkini (dan mungkin kelanjutan saat ini) ke heap, dan kemudian stack dikurangi secara drastis dengan besar setjmp.


Baca juga SICP , Bahasa Pemrograman Pragmatik , Buku Naga , Cincang Dalam Potongan Kecil .


1
Terima kasih banyak. Jika saya dapat menandai kedua jawaban sebagai jawaban yang diterima, saya akan melakukannya, tetapi karena saya tidak bisa, saya akan membiarkannya kosong (tetapi, tidak ingin ada yang berpikir waktu untuk menjawab tidak dihargai)
jleach
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.