- Coroutine menggeneralisasikan subrutin dengan memelihara keadaan setempat dan menyambung semula pelaksanaan pada titik penggantungan, membolehkan ekspresi semula jadi mesin keadaan, penjana dan keserentakan koperatif.
- Pelaksanaan C telah berkembang daripada manipulasi tindanan manual dan API konteks POSIX kepada penghampiran berasaskan makro dan pustaka coroutine mudah alih yang dibina berdasarkan pensuisan konteks peringkat pengguna.
- C++20 menyeragamkan model coroutine tanpa tindanan dengan janji,
co_await,co_yielddan bingkai coroutine, membolehkan pustaka mentakrifkan abstraksi asinkron dan penjana peringkat tinggi. - Model piawai, digabungkan dengan jenis waiters dan janji tersuai, menyatukan penggunaan coroutine merentasi pustaka sambil mengekalkan prestasi dan kawalan yang boleh diramal.
Coroutine terletak di tempat tengah yang menarik antara fungsi klasik dan thread lengkap, dan kisah mereka daripada helah C peringkat rendah kepada sokongan bahasa C++20 yang diseragamkan merupakan salah satu evolusi paling menarik dalam pengaturcaraan sistem moden. Jika anda pernah cuba mengimbangi panggilan balik, mesin keadaan dan penyegerakan thread hanya untuk mengendalikan I/O tanpa penyekatan, anda telah pun menghadapi jenis kesakitan yang direka bentuk untuk melegakan coroutine.
Dalam artikel ini, kita akan membincangkan bagaimana coroutine berkembang daripada penggodaman C buatan tangan dan API konteks POSIX kepada model coroutine C++20 tanpa tindanan peringkat tinggi. menjelaskan apa sebenarnya coroutine, bagaimana ia berbeza daripada penjana, benang dan gentian, apa yang dimaksudkan dengan "stackful" vs "stackless", dan bagaimana jentera C++20 (objek janji, pemegang coroutine, co_await, co_yield, co_return) sebenarnya berkelakuan secara tersembunyi.
Apakah sebenarnya coroutine?
Tiada definisi formal tunggal yang diterima secara universal bagi coroutine, tetapi literatur ini menumpukan pada dua sifat utama yang membezakan coroutin daripada subrutin biasa:
- Negeri tempatan bertahan walaupun terdapat penggantungan: Data setempat kepada coroutine kekal antara pengaktifan, jadi setiap tika coroutine berkelakuan seperti objek dengan memori.
- Pelaksanaan boleh dijeda dan kemudian disambung semula dari titik yang sama: Apabila kawalan meninggalkan coroutine, ia boleh dimasukkan semula pada titik penggantungan yang tepat dan bukannya sentiasa bermula dari atas seperti fungsi biasa.
Daripada mempunyai satu kemasukan dan keluar sekali sahaja seperti subrutin, coroutin menyokong berbilang titik kemasukan dan keluar sepanjang hayatnya, yang menjadikannya berkuasa untuk menyatakan pengeluar, pengguna, mesin keadaan, penjadual koperatif dan aliran kerja tak segerak dalam gaya linear dan boleh dibaca.
Dimensi teras reka bentuk coroutine
Sistem coroutine dunia sebenar berbeza-beza mengikut tiga paksi penting yang menentukan bagaimana ia bertindak dan betapa ekspresifnya: model pemindahan kawalan, sama ada coroutin ialah nilai kelas pertama dan sama ada ia bertindan atau tidak bertindan.
Pertama, mekanisme pemindahan kawalan memisahkan coroutin asimetri daripada coroutin simetri. Dalam reka bentuk asimetri, coroutine aktif hanya boleh menghasilkan kembali kepada pemanggil langsungnya (menggunakan operasi yang secara konseptualnya serupa dengan yield), dan pemanggil menyambungnya kemudian (dengan operasi yang serupa dengan resume). Dalam reka bentuk simetri, coroutine boleh memindahkan kawalan secara eksplisit kepada mana-mana coroutine lain, dan bukannya sentiasa kembali kepada sesiapa yang memanggilnya.
Kedua, sesetengah bahasa menganggap contoh coroutine sebagai objek kelas pertama yang boleh anda simpan, edarkan dan manipulasikan dengan bebas, manakala yang lain hanya mendedahkan coroutine sebagai binaan sintaksis dengan cara terhad untuk berinteraksi dengannya. Sokongan kelas pertama meningkatkan fleksibiliti dan kebolehkomposanan secara mendadak.
Ketiga, coroutine mungkin bertindan atau tidak bertindan. Coroutine bertindan boleh digantung jauh di dalam tindanan panggilan bersarang; apabila ia disambung semula, setiap bingkai dalam tindanan itu akan berterusan di tempat ia berhenti. Coroutine tanpa tindanan hanya digantung pada tahap fungsi coroutine itu sendiri: fungsi pembantu biasa tidak boleh dihasilkan melainkan ia sendiri adalah coroutine atau dianotasi khas.
Istilah "coroutine penuh" telah dicadangkan untuk kombinasi yang paling ekspresif: coroutine kelas pertama yang bertindan, sama ada simetri atau asimetri, yang cukup berkuasa untuk menyatakan kesinambungan sekali gus atau kesinambungan yang dipisahkan. Walaupun gaya simetri dan asimetri mempunyai kuasa ekspresif yang setara, model asimetri selalunya terasa lebih 'seperti rutin' dan biasa bagi kebanyakan pengaturcara.
Subrutin, coroutin, penjana dan thread
Subrutin boleh dilihat sebagai kes khas coroutin dengan aliran kawalan dan tingkah laku keadaan yang sangat terhad. Fungsi normal sentiasa bermula pada arahan pertamanya, keluar sekali dan membuang keadaan setempatnya selepas itu. Sebaliknya, coroutine boleh memindahkan kawalan kepada coroutine lain, disambung semula kemudian pada titik hasil dan mengekalkan keadaannya merentasi pemindahan ini. Pelbagai contoh coroutine bagi fungsi yang sama mungkin wujud bersama, setiap satu dengan data setempatnya yang dipelihara sendiri.
Penjana membentuk subset coroutine yang ketara, kadangkala dipanggil "semicoroutine". Seperti coroutine, penjana boleh menggantung pelaksanaan beberapa kali dan kemudian meneruskan, tetapi ia sentiasa kembali kepada pemanggil langsung mereka dan tidak mempunyai cara untuk mengalihkan pelaksanaan kepada coroutine ketiga yang sewenang-wenangnya. Kekangan itu disengajakan: penjana dioptimumkan untuk melaksanakan iterator dan jujukan malas di mana setiap yield bermaksud "menghasilkan nilai untuk sesiapa yang mengulangi saya".
Malah, anda boleh meniru coroutine umum dengan meletakkan penghantar di atas sistem penjana, contohnya dengan mempunyai trampolin peringkat atas yang menerima token daripada penjana dan memutuskan penjana yang hendak diaktifkan seterusnya. Teknik ini secara sejarahnya digunakan dalam bahasa seperti versi Python awal, yang hanya mempunyai penjana tetapi tiada primitif coroutine terbina dalam.
Coroutine sering dibandingkan dengan thread, tetapi pada asasnya ia adalah tentang penjadualan koperatif dan bukannya paralelisme pencegahan. Coroutine menyediakan keserentakan dalam erti kata tugasan saling berkaitan tanpa mengubah semantik keseluruhan, namun ia tidak berjalan serentak pada berbilang teras dengan sendirinya. Coroutine hanya menghasilkan kawalan pada titik penggantungan eksplisit, jadi kod antara titik tersebut berjalan tanpa gangguan daripada coroutine lain.
Model kerjasama ini menghapuskan banyak masalah penyegerakan yang biasa berlaku dengan thread: Oleh kerana hanya satu coroutine yang berjalan pada satu masa pada penjadual yang diberikan, anda selalunya tidak memerlukan mutex atau operasi atom untuk keadaan kongsi biasa. Sebaliknya, coroutine sendiri tidak akan mengeksploitasi berbilang teras CPU melainkan anda menggabungkannya dengan thread atau pelaksana berbilang thread.
Contoh coroutine klasik: pengeluar-pengguna
Demonstrasi buku teks bagi coroutin simetri ialah corak pengeluar-pengguna dengan barisan kongsi. Satu coroutine menjana item dan menolaknya ke dalam barisan sehingga penuh, kemudian menyerahkannya kepada pengguna; pengguna memasukkan item sehingga barisan kosong, kemudian menyerahkannya kembali kepada pengeluar. Pelaksanaan melantun ke depan dan ke belakang apabila setiap pihak secara kerjasama memberikan kawalan.
Dalam pelaksanaan sedemikian, pengeluar dan pengguna kelihatan berjalan "selari" dari perspektif pengaturcara, walaupun sebenarnya ia hanya melompat ke depan dan ke belakang dalam satu thread pelaksanaan. Tidak perlu thread peringkat OS atau suis konteks: operasi hasil boleh menjadi lompatan peringkat rendah yang menyambung semula bingkai tindanan aktif.
Contoh ini kerap digunakan untuk memperkenalkan multithreading, tetapi penting untuk diperhatikan bahawa coroutine sahaja sudah mencukupi untuk menyatakan logiknya, dan penggantiannya dengan thread mungkin tidak perlu atau berbahaya dalam persekitaran yang mementingkan jaminan masa nyata atau overhed masa jalan yang minimum.
Mengapa coroutine penting: mesin keadaan, pelakon dan aliran kerja asinkron
Oleh kerana coroutine mengekalkan kedua-dua titik pelaksanaan dan pembolehubah setempatnya merentasi hasil, Ia menyediakan cara yang sangat semula jadi untuk melaksanakan mesin keadaan yang kompleks tanpa pernyataan suis, bendera atau kaunter program eksplisit yang meluas. Titik penggantungan semasa secara literalnya mewakili keadaan semasa.
Coroutine juga sesuai untuk model serentak gaya pelakon, seperti yang digunakan dalam banyak enjin permainan. Setiap pelakon boleh dilaksanakan sebagai coroutine yang secara berkala memberikan kawalan kembali kepada penjadual pusat, yang menjalankan satu pelakon demi satu pada satu thread. Multitasking koperatif ini menghapuskan keperluan untuk kebanyakan penguncian sambil tetap menyediakan tingkah laku responsif.
Penjana yang dibina pada coroutine sesuai untuk berfungsi dengan strim dan traversal struktur data, terutamanya apabila anda mahukan penghasilan nilai yang malas dan atas permintaan. Daripada menolak nilai kepada pengguna, penjana membolehkan pengguna menarik nilai satu persatu menggunakan gelung mudah.
Coroutine juga menyerlah dalam corak komunikasi seperti saluran paip dan proses berjujukan komunikasi (CSP), di mana setiap peringkat merupakan coroutine yang mengalah apabila ia menunggu input atau output. Penjadual kemudian menyambung semula coroutine apabila saluran komunikasi mereka sedia, memberikan alternatif yang elegan kepada gelung peristiwa yang banyak panggilan balik.
Akhir sekali, banyak perpustakaan berangka menggunakan gaya yang kadangkala dipanggil "komunikasi terbalik", di mana penyelesai menggantung dirinya sendiri apabila ia memerlukan pengguna untuk memberikan beberapa penilaian fungsi, kemudian menyambung semula sebaik sahaja pengguna memberi respons. Coroutine menyediakan cara langsung dan boleh dibaca untuk menyatakan aliran kawalan bolak-balik itu.
Daripada pelaksanaan C peringkat rendah kepada perpustakaan mudah alih
Satu kelas pelaksanaan memperoleh tindanan panggilan kedua secara manual dan kemudian menggunakannya setjmp/longjmp untuk bertukar antara coroutine. Perhimpunan sebaris khusus platform boleh menyediakan tindanan baharu untuk setiap coroutine; pada sistem POSIX, isyarat digabungkan dengan sigaltstack boleh digunakan untuk melaksanakan bootstrap pada tindanan alternatif dalam C tulen. Sebaik sahaja setiap coroutine mempunyai tindanannya sendiri, setjmp menyimpan keadaan CPU dan penunjuk tindanan, dan longjmp memulihkannya untuk menyambung semula coroutine.
Sesetengah pustaka C sejajar POSIX dan UNIX yang didedahkan secara sejarah mempunyai fungsi pembantu seperti getcontext, setcontext, makecontext dan swapcontext, yang secara langsung merangkumi idea pertukaran antara konteks peringkat pengguna. Walaupun ini telah ditanda usang dalam POSIX.1-2008, ia membentuk tulang belakang beberapa perpustakaan coroutine dan memberi inspirasi kepada reka bentuk kemudian.
Pintasan pelaksanaan coroutine minimum setjmp/longjmp dan API konteks sepenuhnya, sebaliknya memilih pemasangan tulisan tangan yang hanya menukar kaunter program dan penunjuk tindanan, sekali gus merosakkan daftar lain. Ini boleh menjadi lebih pantas pada sesetengah ABI kerana ia menyimpan apa yang diperlukan dan tidak lebih daripada itu, manakala setjmp perlu menyimpan set daftar yang lebih besar secara konservatif.
Untuk menyembunyikan semua kerumitan ini daripada kod aplikasi, pelbagai pustaka C muncul selama bertahun-tahun yang membungkus coroutine bertukar kepada API bersih, seperti Russ Cox libtask dan pelbagai lagi (libpcl, coro, lthread, libcoro, libaco, libco dan banyak lagi). Pustaka ini biasanya menyediakan abstraksi seperti tugasan ringan atau gentian yang boleh disambung semula dan dihasilkan tanpa pemanggil perlu risau tentang helah pemasangan yang mendasarinya.
Anggaran coroutine dalam C menggunakan makro
Jika susunan berasingan atau API penukaran konteks tidak tersedia atau tidak diingini, pembangun juga telah menganggarkan coroutine dalam C tulen dengan makro dan pernyataan suis, satu teknik yang terkenal didokumentasikan oleh Simon Tatham dan berkaitan dengan helah klasik “peranti Duff”.
Idea terasnya adalah untuk mengekod keadaan coroutine sebagai kaunter program yang dilaksanakan dengan switch dan case label, di mana masing-masing yieldMakro seperti -mengembang kepada kod yang merekodkan label semasa dalam pembolehubah statik dan kemudian kembali kepada pemanggil. Pada panggilan seterusnya, fungsi tersebut akan kembali ke label tersebut dan bukannya bermula pada permulaan.
Pustaka seperti Protothreads membina corak ini untuk menyediakan coroutine tanpa tindanan yang sangat ringan yang sesuai dalam persekitaran terbenam yang terhad, tetapi pendekatan ini datang dengan batasan yang serius: pembolehubah tempatan tidak secara semula jadi berterusan merentasi hasil melainkan ia disimpan dalam struktur statik atau luaran, anda tidak boleh menggantung dengan mudah daripada panggilan fungsi bersarang, dan anda biasanya hanya mempunyai satu titik masuk.
Malah penyokongnya menggambarkan helah berasaskan makro ini sebagai antara kod C paling hodoh yang pernah digunakan dalam pengeluaran, dan pengkritik menunjukkan bahawa aliran kawalan yang terhasil mungkin sukar untuk dipertimbangkan dan dikekalkan dari semasa ke semasa. Walau bagaimanapun, ia kekal sebagai kompromi yang berguna dalam sistem di mana susunan tambahan atau helah penghubung tidak dapat dipertimbangkan.
Batu loncatan: gentian, benang dan abstraksi berkaitan
Dalam persekitaran arus perdana yang kekurangan coroutine asli, thread (dan, pada tahap yang lebih rendah, gentian) telah menjadi blok binaan lalai untuk keserentakan, walaupun tingkah laku kerjasama sudah memadai. Thread selalunya disokong dengan baik dan didokumentasikan dengan baik, tetapi ia menyelesaikan masalah yang lebih luas dan lebih kompleks daripada yang sebenarnya diperlukan oleh kebanyakan kes penggunaan coroutine.
Gentian, jika ada, menunjukkan padanan yang lebih hampir dengan coroutine peringkat pengguna kerana ia dijadualkan secara kerjasama dan boleh ditukar tanpa penglibatan OS, menjadikannya substrat semula jadi untuk melaksanakan API gaya coroutine. Walau bagaimanapun, sokongan sistem untuk gentian adalah tidak sekata berbanding dengan thread, dan kebolehgunaan terjejas.
Satu perbezaan ketara antara thread dan coroutine ialah tingkah laku penjadualan. Thread biasanya didahului pada titik sewenang-wenangnya, yang memaksa pengaturcara untuk membuat pertimbangan tentang keadaan perlumbaan dan penyegerakan di mana-mana. Sebaliknya, Coroutine hanya menukar kawalan pada titik penggantungan eksplisit, yang selalunya membolehkan anda menulis kod yang lebih ringkas tanpa kunci atau operasi atom.
Bahasa dan runtime telah meneroka pelbagai laluan untuk meniru coroutine di atas infrastruktur sedia ada, daripada menulis semula bytecode (seperti dalam beberapa rangka kerja coroutine Java) kepada pemetaan binaan seperti coroutine kepada iterator (seperti yang dilakukan oleh C# dengan yield sebelum async/await) atau membinanya di atas benang, sambungan atau gentian hijau.
Coroutine merentasi bahasa pengaturcaraan
Selama beberapa dekad, banyak bahasa telah bereksperimen dengan binaan seperti coroutine, setiap satunya dengan citarasa dan pertukarannya sendiri, dan memahami ekosistem ini membantu meletakkan evolusi C++ dalam konteks.
Sesetengah bahasa menawarkan coroutine bertindan kelas pertama secara langsung dalam pustaka masa jalan dan standard. Lua, sebagai contoh, telah menyokong coroutine bertindan dan tidak simetri sejak versi 5.0 melalui piawaiannya coroutine API, dengan primitif untuk mencipta, menyambung semula dan menghasilkan. Modula-2 secara sejarahnya merangkumi sokongan coroutine melalui prosedur seperti NEWPROCESS dan TRANSFER yang menyediakan tindanan berasingan dan bertukar antara konteks.
Ekosistem lain membina coroutine di atas primitif sedia ada seperti kesinambungan atau benang hijau. Racket (dan dialek Skema secara amnya) boleh melaksanakan coroutine dengan mudah kerana ia mendedahkan sambungan sebagai nilai kelas pertama. Sistem Smalltalk, di mana tindanan pelaksanaan merupakan objek yang boleh dimanipulasi, juga boleh mengehos abstraksi coroutine tanpa sokongan VM tambahan. Dalam OCaml, keserentakan koperatif telah disediakan melalui modul yang menjadualkan thread secara pra-emptif pada thread OS tunggal, manakala versi yang lebih baharu menambah sokongan gaya thread hijau.
Bahasa yang tertumpu pada pengaturcaraan tak segerak sering dimulakan dengan penjana sebelum memperkenalkan coroutine penuh. C# pada mulanya menambah penjana melalui yield dan corak iterator, kemudian berkembang menjadi async/await untuk memodelkan operasi tak segerak sebagai coroutin. JavaScript mengikuti laluan yang serupa: ES2015 memperkenalkan penjana sebagai kes khas coroutin, dan versi yang lebih baharu ditambah async/await dibina di atas janji dan penjana.
Dalam dunia JVM, Java sendiri tidak menawarkan coroutine asli, tetapi alat dan bahasa di sekelilingnya mengisi jurang tersebut. Sesetengah perpustakaan mengubah suai bytecode untuk mensimulasikan tingkah laku coroutine, yang lain menggunakan JNI untuk mengakses mekanisme khusus platform, dan sesetengahnya bergantung pada thread untuk mensimulasikan semantik coroutine pada kos yang lebih tinggi. Kotlin, sebaliknya, menyediakan coroutine sebagai ciri perpustakaan pihak pertama dan boleh berinteraksi dengan kod Java (walaupun Java tidak boleh secara semula jadi "menggantung" dan sebaliknya mesti menyekat atau menggunakan futures).
Skrip dan bahasa dinamik telah mengambil pelbagai pendekatan. Python bermula dengan penjana yang dipertingkatkan (PEP 342), melanjutkannya dengan pendelegasian subpenjana (PEP 380), dan akhirnya memperkenalkan coroutine asli eksplisit dengan async/await (PEP 492), kemudian menempah kata kunci tersebut dalam Python 3.7. Ruby melaksanakan tingkah laku seperti coroutine melalui gentian; Raku dan Tcl menawarkan binaan coroutine asli; PHP 8.1 menambah gentian untuk menyokong pustaka berasaskan coroutine untuk I/O tak segerak.
Bahasa berorientasikan sistem juga meneroka model seperti coroutine dengan kelainannya sendiri. Go menggunakan goroutin — proses ringan dan bermultipleks dengan tindanan bersaiz dinamik. Walaupun goroutin bukanlah coroutin dalam erti kata yang ketat (ia lebih hampir dengan thread hijau, dan data setempat tidak dapat bertahan dalam berbilang 'panggilan' dalam erti kata coroutin), ia menempati ruang mental yang serupa dengan tugas peringkat pengguna yang diuruskan oleh penjadual masa jalan. D mendedahkan coroutin melalui Fiber dalam pustaka piawainya, dan sesetengah rangka kerja membungkusnya ke dalam antara muka gaya penjana yang mudah.
Masukkan pustaka C++: sebelum standard
Sebelum coroutine piawai C++, ekosistem bergantung pada pustaka pihak ketiga untuk membawa semantik coroutine ke dalam bahasa tersebut, menggunakan campuran pensuisan konteks penghimpun, API platform dan metaprogramming templat pintar.
Boost.Context muncul sebagai asas peringkat rendah untuk menukar konteks pelaksanaan merentasi pelbagai seni bina dan sistem pengendalian, menyediakan cara mudah alih untuk memanipulasi susunan ruang pengguna. Selain itu, Boost.Coroutine dan kemudian Boost.Coroutine2 menawarkan abstraksi coroutine peringkat lebih tinggi, beralih daripada sokongan untuk bentuk simetri dan asimetri kepada antara muka asimetri yang lebih moden yang lebih sejajar dengan idiom C++ kontemporari.
Projek lain meneroka sudut yang berbeza, seperti coroutine tanpa tindanan berasaskan prapemproses yang meniru await/yield semantik, pustaka pengepala tunggal yang membalut gentian platform, atau rangka kerja (seperti coroutine Mordor atau Oat++) yang memberi tumpuan khusus untuk menyembunyikan panggilan balik I/O tak segerak di sebalik kod berjujukan seperti coroutine.
Ekosistem ini menunjukkan bahawa pembangun C++ dahagakan ekspresif seperti coroutine, tetapi ia juga mendedahkan titik kesukaran penyelesaian ad-hoc: sintaks yang tidak konsisten, kebimbangan kebolehgunaan yang rumit, penyahpepijatan dan perkakasan yang janggal, dan integrasi yang tidak remeh dengan seluruh pustaka standard.
Coroutine C++20: model tanpa tindanan yang piawai
C++20 akhirnya membawa coroutine ke dalam bahasa tersebut sebagai ciri kelas pertama, tetapi dengan reka bentuk yang sengaja dibuat secara minimum dan peringkat rendah. Daripada menghantar abstraksi peringkat tinggi tertentu (seperti "tugas", "penjana" atau "masa depan") ke dalam pustaka standard, C++20 menyeragamkan blok binaan yang membolehkan pustaka menentukan jenis mesra coroutine mereka sendiri.
Fungsi menjadi coroutine jika badannya mengandungi mana-mana binaan khusus coroutine: yang co_await pengendali untuk menggantung sehingga sesuatu peristiwa atau nilai sedia, co_yield ungkapan untuk menghasilkan nilai dan menggantung (seperti dalam penjana), atau co_return pernyataan untuk melengkapkan coroutine, secara pilihan dengan hasil.
Sebaik sahaja pengkompil mengesan mana-mana binaan ini, ia akan mengubah fungsi tersebut menjadi mesin keadaan yang keadaannya yang berterusan disimpan dalam "bingkai coroutine" yang diperuntukkan timbunan, melainkan pengoptimuman membuktikan bahawa jangka hayat bingkai bersarang sepenuhnya dalam pemanggil dan boleh dibenamkan ke dalam bingkai tindanan pemanggil. Bingkai ini menyimpan objek janji, salinan argumen, pembolehubah setempat yang berada merentasi titik penggantungan dan metadata pembukuan untuk menyambung semula pelaksanaan.
Yang penting, coroutine C++20 tidak bertindan: coroutine hanya boleh digantung pada titik penggantungan yang jelas (co_await or co_yield) dan tidak boleh menghasilkan secara telus dari dalam panggilan bersarang sewenang-wenangnya melainkan fungsi tersebut juga merupakan coroutine atau terlibat dalam jentera coroutine. Ini menjadikan pelaksanaan lebih mudah dan lebih boleh diramal, dengan mengorbankan sedikit kuasa ekspresif berbanding reka bentuk bertindan sepenuhnya.
Sekatan dan kitaran hayat coroutine C++20
Tidak semua fungsi dalam C++20 dibenarkan menjadi coroutine; piawaian mengenakan beberapa kekangan untuk memastikan model waras. Coroutine tidak boleh constexpr or consteval fungsi, ia tidak boleh menjadi pembina, pemusnah atau main fungsi, dan mereka mungkin tidak menggunakan argumen variadik gaya C atau jenis pulangan ruang letak seperti biasa auto tanpa spesifikasi tambahan.
Apabila coroutine pertama kali dipanggil, ia tidak serta-merta bertindak seperti badan fungsi biasa. Sebaliknya, prolog yang dihasilkan oleh pengkompil memperuntukkan bingkai coroutine (biasanya melalui operator new), menyalin parameter fungsi ke dalam bingkai tersebut (mengikut nilai atau rujukan seperti yang diisytiharkan), membina objek janji dan kemudian memanggil promise.get_return_object(), yang biasanya menghasilkan beberapa objek pemegang atau pembalut yang dikembalikan kepada pemanggil.
Objek janji ialah jenis yang ditentukan pengguna, ditemui melalui std::coroutine_traits berdasarkan jenis pulangan dan senarai parameter coroutine, dan ia menentukan bagaimana keputusan, pengecualian dan dasar penggantungan berfungsi. Pengkompil menyimpulkan a Promise taip dan kemudian memanggil kaedah seperti initial_suspend(), final_suspend(), return_value() or return_void(), dan unhandled_exception() pada fasa yang sesuai dalam kitaran hayat coroutine.
Pada permulaan pelaksanaan, coroutine memanggil promise.initial_suspend() dan co_awaitapa sahaja yang kembali, yang membolehkan penulis perpustakaan memutuskan sama ada jenis coroutine mereka adalah "bersemangat" (mula berjalan serta-merta) atau "malas" (kembali kepada pemanggil sehingga disambung semula secara eksplisit). Apabila coroutine akhirnya selesai melalui co_return atau pengecualian yang tidak dikendalikan, ia memanggil promise.final_suspend(), memberi perpustakaan peluang terakhir untuk menjadualkan sambungan atau pembersihan.
Apabila bingkai coroutine dimusnahkan — sama ada selepas siap atau melalui operasi pemusnahan eksplisit pada pemegangnya — runtime memusnahkan objek janji, salinan parameter dan mana-mana penduduk setempat langsung yang tinggal, kemudian membebaskan memori dengan operator delete (atau dengan peruntukan khusus janji jika disediakan). Jika peruntukan gagal dan janji tersebut menentukan get_return_object_on_allocation_failure(), coroutine boleh memberi isyarat kegagalan dengan anggun tanpa membuang std::bad_alloc.
co_await, yang dinanti-nantikan dan yang menunggu
. co_await operator ialah primitif penggantungan utama dalam sistem coroutine C++20, dan memahami mekaniknya adalah penting untuk mereka bentuk abstraksi tak segerak yang teguh.
Semasa anda menulis co_await expr; di dalam coroutine, pengkompil mula-mula menukar expr menjadi objek "yang dinantikan", sama ada dengan melaluinya promise.await_transform(expr) jika ahli sedemikian wujud, atau dengan menggunakannya sebagaimana adanya. Kemudian ia menentukan objek "waiter" sama ada dengan memanggil ahli operator co_await pada yang ditunggu-tunggu, bukan ahli operator co_await, atau hanya menganggap awaitable itu sendiri sebagai waiter jika tiada operator sedemikian wujud.
Pelayan mesti menyediakan tiga operasi utama: await_ready(), await_suspend(handle) dan await_resume(). If await_ready() mengembalikan benar, coroutine tidak menggantung dan memanggil secara langsung await_resume(), membolehkan laluan pantas untuk operasi yang telah siap. Jika ia mengembalikan palsu, coroutine akan digantung, keadaannya disimpan dalam bingkai, dan await_suspend() dipanggil dengan pemegang kepada coroutine semasa.
Dalam await_suspend(), waiter boleh memutuskan apa yang perlu dilakukan dengan pemegang coroutine: jadualkannya untuk penyambungan semula kemudian pada sesetengah pelaksana, sambung semula coroutine lain, atau sambung semula coroutine yang sama dengan segera (bergantung pada jenis pulangan dan nilai await_suspend()). Apabila operasi yang ditunggu-tunggu selesai, seseorang akhirnya akan menghubungi handle.resume(), pada ketika mana kawalan kembali kepada sebelum await_resume(), Dan kemudian await_resume() menghasilkan hasil daripada co_await ungkapan.
Perpustakaan standard menghantar dua perkara remeh yang boleh dinantikan: std::suspend_always dan std::suspend_never, yang sering digunakan dalam initial_suspend() dan final_suspend() pelaksanaan untuk menunjukkan permulaan yang malas atau bersemangat dan cara bertindak pada akhirnya. Pelayan yang lebih canggih boleh memegang keadaan setiap operasi, contohnya untuk mengikat coroutine ke dalam API I/O tak segerak, dan keadaan itu berada di dalam bingkai coroutine merentasi titik penggantungan.
co_yield dan coroutine gaya penjana
. co_yield ekspresi dibina di atas co_await untuk menyokong tingkah laku seperti penjana, di mana coroutine berulang kali menghasilkan nilai untuk pemanggil yang mengulanginya.
Secara konsep, co_yield value; berkembang menjadi panggilan kepada promise.yield_value(value) dan kemudian penggantungan, biasanya melalui co_await std::suspend_always atau sesuatu yang serupa yang boleh ditunggu. Pelaksanaan janji bertanggungjawab untuk menyimpan nilai yang dihasilkan di tempat yang boleh diakses (dengan menyalin, memindahkan atau merujuknya) supaya pengguna boleh mendapatkannya semula sebelum coroutine disambung semula.
Kod perpustakaan yang melaksanakan penjana biasanya mentakrifkan jenis janji yang mendedahkan kaedah untuk mengakses nilai yang dihasilkan semasa dan untuk mengintegrasikan dengan protokol lelaran standard, seperti menyediakan begin()/end() pada pembalut pemegang dan memajukan coroutine yang mendasari pada setiap kenaikan.
Pengendalian ralat, rujukan tergantung dan butiran halus
Coroutine C++20 disepadukan dengan pengendalian pengecualian C++ melalui promise unhandled_exception() kaedah, yang dipanggil oleh pengkompil jika pengecualian terlepas daripada badan coroutine. Coroutine kemudiannya akan meneruskan penggantungan terakhirnya, dan janji tersebut dijangka akan mengatur agar ralat tersebut disampaikan kepada sesiapa sahaja yang memiliki jenis hasil coroutine.
Oleh kerana parameter disalin atau dirujuk ke dalam bingkai coroutine pada masa penciptaan, Parameter rujukan perlu diberi perhatian: jika ia merujuk kepada objek yang hayatnya tamat sebelum coroutine disambung semula, coroutine mungkin akan membatalkan rujukan yang tergantung. Ini bukanlah masalah khusus coroutine, tetapi sifat bingkai yang berterusan menjadikannya lebih mudah untuk secara tidak sengaja hidup lebih lama daripada objek yang dirujuk.
Piawaian ini juga telah berkembang untuk menjelaskan kes-kes pinggir melalui laporan kecacatan, seperti memastikan tidak sah return_void persediaan yang tidak terbentuk dengan baik dan bukannya menghasilkan tingkah laku yang tidak tertakrif apabila jatuh dari hujung coroutine, dan membenarkan co_await dalam lebih banyak konteks seperti badan lambda.
Bersama-sama, peraturan dan penambahbaikan ini membentuk model peringkat yang agak rendah tetapi boleh diramal yang mana perpustakaan coroutine peringkat yang lebih tinggi boleh dibina dengan selamat, daripada penjana mudah kepada sistem tugasan asinkron/tunggu penuh yang disepadukan dengan pelaksana dan penjadual.
Dilihat dari atas, perjalanan daripada helah pemasangan ad-hoc dalam C kepada coroutine C++20 yang berstruktur, tanpa tindanan dan didorong oleh janji mencerminkan usaha berterusan ke arah abstraksi yang lebih selamat dan boleh dikomposkan untuk menyatakan aliran kawalan yang kompleks, operasi tak segerak dan pengiraan berstatus, tanpa mengorbankan prestasi dan kawalan yang diandalkan oleh pengaturcara sistem.