async/awaitdi Rust menargetkan konkurensi berskala besar yang menangani puluhan ribu koneksi, tetapi berbenturan dengan tujuan Rust berupa kontrol tingkat rendah dan verifikasi lifetime statis, sehingga menciptakan pengalaman pengembangan yang berbeda dari Rust biasa- Thread dan channel sudah cukup untuk banyak perangkat lunak, tetapi pada skala seperti C10K, beban model satu thread per koneksi menjadi besar sehingga dibutuhkan task di user space dan penjadwalan runtime
- Dalam Rust
async, data harus dipindahkan sebagaiSendatau diperlakukan sebagai referensi'static, dan karena sifatasyncyang menular, batasan seperti ini berulang di seluruh kode Arcmemang menyelesaikan masalah kompilasi, tetapi membuat lifetime objek dan resource menjadi kabur; lalu muncul jebakan lain sepertiasyncrekursif, perbedaan future dan task, serta pemanggilan blocking yang memblokir thread runtime- Di Haskell atau Go, “async code” berperilaku seperti kode biasa dan runtime serta GC menyembunyikan perbedaannya, sehingga dalam jenis pemrograman ini kontrol eksplisit Rust mungkin tidak selalu menjadi keuntungan murni
Mengapa konkurensi dan paralelisme dibutuhkan
- Program yang cepat memiliki dua kebutuhan sekaligus
- Harus memanfaatkan seluruh komputer dengan memakai beberapa core CPU
- Harus tetap mengerjakan hal lain saat menunggu operasi lambat seperti pengiriman pesan internet atau membuka file
- Paralelisme adalah persoalan menjalankan kode secara bersamaan di beberapa CPU
- Konkurensi adalah cara memecah masalah menjadi bagian-bagian independen
- Keduanya tidak sama, tetapi jika program dipecah menjadi potongan-potongan konkuren, potongan itu dapat berjalan secara paralel dan terus membuat core sibuk
Proses, thread, dan channel
- Cara sederhana untuk membuat sistem konkuren adalah membagi kode menjadi beberapa proses
- Scheduler sistem operasi menjalankan potongan waktu dari proses yang siap berjalan pada core CPU yang tersedia
- Model ini juga digunakan saat menghubungkan perintah shell dengan pipe
- Pendekatan proses memiliki biaya komunikasi antarproses yang besar
- Dalam banyak implementasi, data harus disalin ke memori OS lalu diambil kembali
- Shared memory dapat mengurangi biaya, tetapi melemahkan keuntungan bahwa OS mengisolasi proses satu sama lain
- Thread berbagi memori yang sama sehingga menghindari overhead ini, tetapi penggunaan alat sinkronisasi seperti mutex, condition variable, dan semaphore secara keliru dapat menimbulkan data race dan deadlock
- Model Communicating Sequential Processes dari Tony Hoare menghubungkan thread dengan antrean atau channel
- Thread tidak berbagi memori, sehingga memperoleh isolasi seperti proses
- Input dan output setiap thread terlihat sebagai channel, sehingga penalaran dan debugging menjadi lebih mudah
- Channel itu sendiri berperan sebagai sinkronisasi; jika kosong penerima menunggu, dan jika penuh pengirim menunggu
- Pustaka standar Rust memiliki std::sync::mpsc::sync_channel
- Banyak perangkat lunak sudah cukup dengan kombinasi alat seperti thread dan channel, serta Rayon untuk memparalelkan loop yang CPU-intensive
Konkurensi user-space dan Rust async
- Pada masalah C10K seperti server web dengan puluhan ribu pengguna terhubung secara bersamaan, pendekatan menempelkan satu thread ke satu koneksi mencapai batasnya
- Di Linux, setiap thread memiliki control block 4 kB, dan perpindahan thread membutuhkan context switch yang masuk ke scheduler sistem operasi
- Untuk konkurensi berskala besar, sebagian bahasa membuat dan mengelola task di user space
- Runtime menjadwalkan task ke pool thread OS
- Biasanya pool dikonfigurasi agar ada satu thread per core CPU untuk memaksimalkan paralelisme
- Pendekatan ini disebut green thread, lightweight thread, lightweight process, fiber, coroutine, dan sebagainya
- Rust menggunakan model
async/awaityang terlihat di C# atau Node.jsasync fntidak langsung mengembalikan nilai, melainkan mengembalikan future atau promise yang hasilnya diperoleh dengan.await
- Future Rust sangat kecil dan cepat berkat penjadwalan kooperatif dan desain stackless
- Rust mencoba menyediakan abstraksi future sekaligus menjanjikan kontrol tingkat rendah kepada programmer
- Rust mencoba memverifikasi lifetime semua objek dan referensi secara statis pada waktu kompilasi
- Future memecah kode dan data yang dirujuk kode itu menjadi ribuan bagian, lalu memungkinkan bagian-bagian itu dijalankan kapan saja dan di thread mana saja bergantung pada kondisi yang baru diketahui setelah eksekusi dimulai
- Future yang membaca data klien harus dijalankan hanya ketika ada data untuk dibaca pada socket tersebut, tetapi lifetime annotation tidak memberi tahu kapan momen itu terjadi
- Rust tidak memasukkan runtime future ke dalam bahasa, melainkan menyerahkannya ke library seperti Tokio
- Pengguna mendapat kebebasan memilih alternatif yang sesuai dengan lingkungannya
- Namun, meski membayangkan dunia tempat Tokio tertanam dalam bahasa, aturan yang sama tetap berlaku, sehingga ini hanyalah detail sekunder dalam argumen ini
Tekanan yang dibuat oleh Send, 'static, dan Arc
- Untuk meyakinkan compiler, data harus ditandai sebagai
Sendagar dapat dipindahkan, atau diteruskan melalui referensi yang memiliki lifetime'static - Dalam kode
async, beberapa task sering berbagi state bersama, sehingga cara memindahkan data tanpa duplikasi kerap tidak cocok - Referensi juga sulit, dan tidak ada padanan seperti
thread::scopeyang membatasi lifetime future menjadi lebih pendek daripada “selamanya” asyncbersifat menular, sehingga fungsi yang memanggil fungsiasyncjuga harus menjadiasync- Karena itu, masalah lifetime dan kemampuan berpindah seperti ini harus diselesaikan terus-menerus, bukan hanya di beberapa fungsi
- Rantai dapat diputus dengan menunggu penyelesaian future memakai
block_ondi runtime, tetapi cara ini tidak composable dan runtime bisa panic jika bersarang
- Arc adalah alat untuk menangani lifetime dinamis di beberapa thread, serta membuat kode lolos borrow check dan dapat dikompilasi
- Namun, penggunaan
Arcsecara luas membuat lifetime objek dan resource menjadi kabur- Tidak jelas kapan resource seperti memori, file, dan socket akan dilepas
- Kita mengalami kerugian mirip GC tanpa keuntungan yang disediakan GC sungguhan, seperti throughput alokasi, fragmentasi rendah, dan penghindaran cycle leak
Jebakan tambahan dalam Rust async
- Coroutine Rust bersifat stackless, sehingga compiler mengubah tiap coroutine menjadi state machine yang bergerak maju sampai titik
.await- Fungsi
asyncrekursif menjadi tipe yang didefinisikan secara rekursif - Pengguna yang sekadar ingin memanggil dirinya sendiri harus melakukan boxing secara manual atau memakai crate seperti async-recursion
- Fungsi
- Future tidak melakukan apa pun sampai di-await
- Task memulai pekerjaan di pool thread runtime dan mengembalikan future yang menandai penyelesaiannya
- Tidak ada mekanisme yang mencegah pemanggilan kode blocking dari dalam future
- Juga tidak ada yang mencegah pemanggilan seperti itu memblokir thread runtime tempat future tersebut berjalan
- Ini bertentangan dengan tujuan utama memakai
async
Perbedaan Rust biasa, Haskell, dan Go
- Rust
asyncterasa sangat berbeda dari Rust “normal”- Ada lebih banyak jebakan
- Lebih sulit dipahami dan diajarkan
- Pengguna berada di antara dua pilihan
- Memahami secara mendalam bagaimana abstraksi benar-benar bekerja lalu menulis kode yang kompleks
- Menaburkan elemen seperti
Arc,Pin, dan'staticdi berbagai tempat dalam kode dan berharap semuanya berjalan baik
- Bahkan tim developer berpengalaman pun dapat tersandung detail-detail seperti ini saat mencoba memakai Rust untuk proyek baru
- Di Haskell atau Go, “async code” adalah kode biasa
- Kedua bahasa menyembunyikan perbedaan antara kode blocking dan non-blocking di balik runtime yang tebal
- Masalah lifetime diserahkan kepada garbage collection
- Dalam perangkat lunak user-space berkala besar dengan banyak pengguna konkuren seperti ini, pendekatan runtime dan GC yang menyembunyikan perbedaan bekerja sebagai keuntungan murni
- Rust mungkin bukan alat yang baik untuk perangkat lunak user-space berskala besar dengan konkurensi tinggi, dan mungkin lebih baik dipakai untuk proyek yang tidak memiliki kebutuhan tersebut
1 komentar
Komentar Hacker News
Saya sedang menulis klien metaverse berperforma tinggi dengan Rust, dan saat ini ukurannya sekitar 40 ribu baris
Video demonya ada di https://video.hardlimit.com/w/tp9mLAQoHaFR32YAVKVDrz
Untuk metaverse yang benar-benar layak, konten buatan pengguna harus diproses hampir secara real time, sehingga butuh VRAM 2–3 kali lebih besar daripada game sejenis; untuk memuat aset dari server dibutuhkan bandwidth ratusan Mbps, beberapa CPU, dan Vulkan untuk menjalankan rendering serta upload ke GPU secara paralel
Ini bukan struktur seperti konkurensi “skala web”, di mana server-server kecil berjalan terpisah dalam address space yang sama, melainkan struktur yang menggerakkan bersama thread render berprioritas tinggi, thread pembaruan event jaringan, thread pemuatan dan dekompresi aset, serta beberapa thread untuk objek bergerak, LOD, pembersihan cache, dan sebagainya
Di Rust, selain konstanta, saya memakai locking yang cukup banyak tanpa state global; channel digunakan di tempat yang tepat, dan pohon objek utama terutama ditangani oleh thread pembaruan dengan kepemilikan tunggal. Koneksi objek grafis dikelola dengan reference count
Arc, lalu mesh dan tekstur diunggah ke GPU melalui Rend3/WGPU/VulkanKalau saya melakukannya dengan C++, mungkin saya akan terus bertarung dengan crash, tetapi di Rust crash terkait memori hanya terjadi kira-kira setahun sekali, dan itu pun biasanya berasal dari kode
unsafemilik orang lain. Di kode saya,unsafedilarang; memang sulit dikompilasi, tetapi setelah berhasil, cenderung “langsung berjalan”, jadi menurut saya jauh lebih baik daripada debugging konkurensiAda juga keluhan. Rust kuat terhadap data race, tetapi tidak bisa mencegah deadlock, jadi diperlukan static analyzer yang melacak urutan lock di sepanjang jalur pemanggilan.
asynctidak cocok untuk pekerjaan yang berpusat pada komputasi dan beberapa thread dengan prioritas berbeda, tetapi terus merembes masuk sebagai dependensi. Struktur umum dengan kepemilikan tunggal plus back-reference terlalu sulit tanpaRcdanWeak, dan sistem trait juga kompleks, sehingga pada bagian pemrosesan aset yang secara alami cocok dengan orientasi objek muncul kode duplikatCrate inti untuk grafis juga belum matang. Pernyataan “di Rust ada 5 game dan 50 game engine” bukan masalah bahasa, melainkan masalah ekosistem; bahkan dibandingkan dengan https://gamedev.rs/, pengembangan game serius dengan Rust masih tampak kurang. Untuk pengembangan game profesional yang punya jadwal, ekosistem game Rust belum siap, dan menurut saya kira-kira masih butuh sekitar 5 orang bekerja satu tahun lagi
async, secara keseluruhan kelebihannya jauh lebih besarSeperti
lockdepdi Linux, kita bisa menganalisis lock lain apa yang diambil saat suatu lock sedang dipegang, dan memberi tahu kombinasi berbahaya bahkan sebelum benar-benar berhenti. Untuk lock yang kompleks mungkin perlu anotasi seperti “kelas lock ini selalu diambil berdasarkan urutan alamat”, tetapi tampaknya bisa diimplementasikanDi luar akses data, race condition masih bisa terjadi: https://news.ycombinator.com/item?id=23599598
Buat beberapa thread pool lalu routing future ke tempat yang sesuai, atau tulis event loop sendiri yang mengambil dari beberapa event queue dengan prioritas berbeda. Cara kedua, jika waktu eksekusi pekerjaan terbatas, dapat memberi jaminan soft real-time untuk pekerjaan berprioritas tinggi sambil tetap memproses pekerjaan berprioritas rendah bahkan ketika CPU 100%
Tentang Rust
async, posisinya agak anehJika memakai banyak
Arc,RwLock, dan shared state, kodenya menjadi berantakan; dan memang benar kritik bahwa terutama ketika'staticmulai menyebar ke mana-mana, ia menginfeksi semuanya seperti function coloring. Dulu saya juga pernah kacau karena menempelkanArcdan mencoba menangani lifetime borrowing dengan pintarNamun Rust juga punya channel. Kode yang saya tulis sekarang sebagian besar berbentuk beberapa task yang melayani channel, melihat pesan yang masuk, lalu jika perlu memasukkan pesan untuk task lain ke channel yang sesuai. Tidak ada berbagi objek. Jika beberapa task membutuhkan objek besar, objek itu ditempatkan di dalam task yang mengirim hasil query terkait sebagai pesan, atau tiap task membuat salinannya sendiri dari aliran pesan
Meski begitu, terlalu banyak artikel yang membahas cara memakai
Arcdan cara menangani lifetime. Kalau Anda mengimplementasikan runtimeasync, itu mungkin diperlukan, tetapi saya kurang paham mengapa pengguna library rata-rata harus begitu fokus pada hal iniasynctidak otomatis berarti multithreading, dan jikaasyncberjalan di thread yang sama, tidak ada sharing, sehingga tidak perlu menempelkan semua keyword ajaib pada hal yang dibagikanSaat berpindah antar-thread, kirim sinyal lewat channel daripada menaruh banyak shared state. Jika benar-benar ada state global yang diperlukan, buat struct kecil yang membungkus mekanisme akses eksklusif seperti
Arc/RwLock, sehingga dari sudut pandang pemanggil terlihat seperti pemanggilan fungsi biasaKekhawatiran soal
Send+Syncjuga kurang saya pahami. Berdasarkan pengalaman, kebanyakan hal dengan mudahSend+Sync, dan yang tidak demikian memang seharusnya tidak boleh atau tidak bisa begitu. Kadang kita memang ingin menulis kode tanpa memikirkan detailnya, tetapi jika membutuhkan konkurensi dan paralelisme yang efisien, mikrodetik dan throughput menjadi penting, dan saat itu kita harus benar-benar menulis kode komputer yang sesuaiNamun cara coding seperti ini sangat berbeda dari JavaScript
asyncyang terasa seperti kode sinkron yang diberi green thread. Orang-orang tampaknya berusaha menulis kode dengan cara yang sudah akrab, sehingga di Rust mereka masuk ke jalurArcdanRwLockMenstrukturkan masalah sebagai data yang mengalir di antara pekerjaan, menghubungkannya dengan queue, dan menghindari shared state adalah cara yang lebih baik untuk menangani multithreading, apa pun bahasa yang digunakan
asyncpada dasarnya adalah Rust yang jauh lebih sulit, dan mungkin hanya sekitar 1% proyek yang benar-benar membutuhkannya, jadi sayang rasanya ketika ini seolah dipaksakan kepada semua orang.Namun, untuk 1% itu, ia benar-benar luar biasa. Untuk layanan seperti linkerd atau nginx yang inti pekerjaannya menangani panggilan jaringan dalam jumlah besar, untuk game yang menjalankan sangat banyak tugas ringan, atau untuk embedded yang membutuhkan concurrency kooperatif, async Rust menjadi senjata yang sangat kuat.
Sebagian besar kode di level sistem dan aplikasi tidak membutuhkan I/O asinkron. Aplikasi REST sudah cukup dengan thread pool, dan kalaupun membutuhkan
async, biasanya model campuran yang membatasinya pada bagian kecil seperti jaringan lalu menghubungkan sisanya dengan thread dan channel adalah pilihan yang tepat.Komunitas Rust memakai
asyncterlalu sembarangan di mana-mana, sehingga Rust dengan I/O blocking yang pengalaman penggunanya lebih baik menjadi warga kelas dua di ekosistem. Untuk web framework pun ada beberapa framework asinkron yang dirancang dengan baik seperti Axum dan Warp, sementara di sisi blocking pilihannya jauh lebih terbatas, sepertitiny_http,rouille, danastra.Dengan memilih coroutine tanpa stack, muncullah masalah
async/awaitdan fungsi berwarna, serta gesekan yang disebutkan dalam tulisan itu. Go tidak punya masalah ini karena memakai coroutine dengan stack.Rust pada awalnya juga mempertimbangkan coroutine dengan stack, tetapi menilai bahwa itu membutuhkan runtime preemptive untuk coroutine dan biayanya besar, lalu memilih model tanpa stack. Namun kebanyakan orang tidak memakai
asyncRust tanpa runtime; mereka memakai Tokio, dan Tokio pada praktiknya melakukan hampir semua pekerjaan runtime yang tadinya ingin dihindari.Akibatnya, banyak pengguna
asyncRust mendapatkan sisi buruk dari kedua pendekatan. Di ranah embedded, ada juga yang memakaiasyncRust dengan runtime yang sangat tipis, tetapi jumlahnya sedikit dan bahkan mereka pun belum sepenuhnya yakin.reqwest, lalu itu membawah2, lalu itu membawatokiolagi.asyncjika platform mendukung virtual thread.Sebagai pengguna Java, saya sedang berusaha membuang seluruh paradigma asinkron dan menulis ulang kode ke model blocking di atas virtual thread, karena blocking di sana tidak masalah.
asyncsudah menyebar ke terlalu banyak crate, sehingga seluruh program menjadiasyncatau setidaknya bergantung pada Tokio untuk banyak hal.Kalau ingin web server, kesannya pilihannya cuma
async + tokioatau pergi saja, dan untuk konektor SQL pun suasananya seperti harus menulis sendiri kalau tidak mau yang asinkron. Setiap orang menyelesaikan masalah yang dibawaasyncdengan cara berbeda-beda, dan hal seperti closureasyncterasa seperti membuka gerbang neraka di compiler.Memang bagus bahwa Rust sendiri dan compilernya membantu memecahkan masalah, tetapi ekosistem yang hampir seperti “kalau bukan
async, buat sendiri” itu belum cukup.futures, banyak rasa sakit ini bisa berkurang.Diperlukan hal seperti trait yang harus diimplementasikan executor, atau executor blocking bawaan untuk menjalankan kode asinkron dari kode sinkron. Saat ini membuat library yang mendukung beberapa runtime asinkron saja sudah menyiksa, sehingga akhirnya hanya mendukung Tokio, atau paling banter menambahkan
async-std.Saya bukan pakar
asyncRust, tetapi setelah menulis beberapa ribu baris Rust sinkron bulan ini, yang saya rasakan adalah ketikarustcmembuat suatu pendekatan terasa sulit, biasanya ada alasan yang masuk akal, dan ada cara lain untuk mendapatkan hasil serupa dengan lebih baik.Kalau sedang belajar bahasa ini, saya menyarankan untuk lebih dulu terbiasa dengan kode sinkron biasa, loop dan conditional, serta aturan borrowing.
asyncmasih banyak berkembang, bukan hanya dalam implementasi, tetapi juga pada level filosofis tentang “apa itu asinkron dan bagaimana seharusnya terlihat bagi pengguna”.Compiler sangat bergantung pada trait, tetapi kemampuan trait untuk menangani
asyncbelum distabilkan. Misalnya ada pekerjaan seperti https://blog.rust-lang.org/inside-rust/2022/11/17/async-fn-i....Jika fitur asinkron pada trait belum stabil, menyerang kode asinkron Rust karena belum rapi pada akhirnya mirip dengan mengkritik draf awal sebuah buku yang nantinya akan selesai.
Saya juga memikirkan bagaimana mencegah asinkron menyebar ke seluruh codebase.
Rancangan saya saat ini adalah struktur tempat thread I/O membagi event sistem dari
liburingatauepollke dua tahap, “submit” dan “handle”, lalu mengirimkannya ke komponen lain. Misalnya jika membuattcp-connection, kita bisa berlangganan event asinkron seperti “siap ditulis” dan “siap dibaca”; event siap tulis mengambil data dari buffer yang diisi dengan mutex biasa, lalu mengirimkannya denganEPOLLOUT/io_uring_prep_writev.Untuk pengiriman event antar-thread, bisa dipakai ring buffer multi-producer multi-consumer dengan pola LMAX Disruptor. Thread aplikasi atau thread pool masing-masing memiliki event loop sendiri dan memproses ring buffer ini.
Saya juga sedang mengerjakan sintaks untuk mengekspresikan urutan pemicu event asinkron; bentuknya mirip pipeline Bash dan disebut
statelines:initialstate1 initialstate2 = state1 | {state1a state1b state1c} {state2a state2b state2d} | state3Masa pakai
Arcbukan tidak bisa diketahui; ia ditentukan oleh di mana dan bagaimana ia dipegangKesenjangan dalam tulisan ini tampaknya berasal dari penulis yang, alih-alih mempelajari Rust dan bekerja sesuai dengan bahasanya, mencoba memaksakan model berpikir lama seperti garbage collection ke Rust. Ini jebakan umum saat mempelajari bahasa baru, tetapi Rust khususnya membuat orang lebih sering tersandung
Namun itu hampir kebalikan dari tujuan borrow checker yang ingin membatasi masa pakai objek secara statis pada waktu kompilasi
Dalam praktiknya, yang saya alami justru hampir sebaliknya. Setelah sekitar 10 tahun melakukan pemrograman sistem dengan C, C++, dan Rust, di tempat kerja sekarang saya banyak memakai Haskell, dan cukup membuka mata bahwa runtime bahasa yang besar dan garbage collection bukanlah monster di beberapa ranah masalah
asyncmenghambat optimisasi yang bisa dilakukan compiler pada kode non-asinkronBagian tentang bergulat dengan
Weaktampaknya menunjukkan upaya membuat struktur ownership yang kompleks, dan itu memang tidak mudah di Rust secara umum. Saya sangat jarang memakai weak smart pointerChannel hampir tidak disebutkan, padahal itu adalah alat utama untuk membuat berbagai bagian program saling berkomunikasi, baik dalam kode asinkron maupun saat menjembatani kode asinkron dan sinkron. Ada juga abstraksi sinyal seperti
Notifydan semaphoreMutex lambat dan mudah menjadi bottleneck, sedangkan shared state cepat menjadi rumit. Ini sudah lama diketahui. Masalahnya mungkin sejak awal ada pada struktur seperti
BIG_GLOBAL_STATIC_REF_OR_SIMILAR_HORRORKritik bahwa Rust tidak bisa mencegah pemanggilan kode blocking dari konteks asinkron memang valid, tetapi jika diperlukan hal itu relatif bisa dikelola dengan sesuatu seperti
tokio::spawn_blockingPenulis kemungkinan besar tahu apa itu
Arcdan bagaimana cara kerjanya; poinnya lebih dekat ke bahwa di Rustasync, dibanding kode sinkron, Anda jauh lebih sering memakaiArcalih-alih RAII biasaJika 90% objek program memakai reference counting, mungkin lebih baik memakai tracing garbage collection daripada membayar biaya banyak alokasi dan dealokasi heap kecil serta operasi atomik. Contoh tutorial Tokio juga menunjukkan arah serupa: https://tokio.rs/tokio/tutorial/shared-state
Saya penasaran apakah tracing garbage collection sungguhan di Rust bisa membuat aplikasi asinkron umum seperti server HTTP menjadi jauh lebih cepat: https://manishearth.github.io/blog/2015/09/01/designing-a-gc...
Arcbukan acak, melainkan tidak bisa diketahui secara statisArcdi Rust bisa dipindahkan atau dipinjam, dan juga bisa digunakan tanpa menyentuh reference countDalam banyak kasus, biayanya jauh lebih murah daripada objek dalam bahasa dengan reference counting implisit
Saya suka Rust, tetapi
asyncitu berantakan, dan kita tidak bisa menulis kode asinkron seperti menulis kode sinkronSaya makin yakin bahwa mencampurkan keduanya adalah ide buruk, dan mungkin pendekatan ala Go—membiarkan semuanya sinkron dan hanya menyediakan satu primitive channel
async—adalah yang tepatSaat ini saya sedang merangkai logika agar sebuah struct yang mengimplementasikan
Futurememanggil method sinkron, dan ini tantangan yang cukup menarik. Abstraksi asinkron zero-cost mungkin bisa dibuat cukup mudah bagi pengguna sampai batas tertentu, tetapi rasa sakitnya akan ditanggung oleh pengembang libraryasyncjelas menyakitkan juga bagi pengguna akhir, rasanya seperti memakai bahasa terpisah yang kehilangan fitur inti Rust seperti lifetime dan tipe eksplisit, lalu ditaburi banyakPinKarena tidak bisa menjalankan fiber yang memiliki scope, akhirnya kita menempelkan banyak
Arc;Pinsulit dipakai tanpaunsafe; dan perubahan sangat kecil pada fungsi asinkron bisa membuat future di seluruh codebase menjadi!Sendasync. Kita lihat saja bagaimana perkembangannyaAsync Everything adalah bahasa yang buruk
async/awaitadalah ide mengerikan yang awalnya mencoba memperbaiki masalah JavaScript yang tidak punya thread blocking yang layak, dan sekarang ditempelkan ke semua bahasa. Ini akan membelah ekosistem bahasa dan library menjadi dua, lalu menciptakan penderitaan untuk waktu yang lamaSiapa pun yang pernah melakukan multithreading di luar JavaScript tahu bahwa aktor atau communicating sequential processes adalah cara terbaik untuk multithreading
Dalam tesis Joe Armstrong juga dijelaskan bahwa satu-satunya cara memahami program multithread adalah menulis kode yang benar-benar sekuensial untuk tiap thread, dan tidak mencampuradukkan kode dari banyak thread di satu tempat. Satu aktivitas konkuren nyata dari masalah harus tepat berpadanan dengan satu proses konkuren dalam bahasa pemrograman agar kesenjangan konseptual menjadi minimal: https://erlang.org/download/armstrong_thesis_2003.pdf
Kritik Ron Pressler, yang mengimplementasikan Project Loom di Java, terhadap
async/awaitjuga bagus: https://www.youtube.com/watch?v=oNnITaBseYQIa sendiri cukup ambivalen terhadap JavaScript, dan tujuan utamanya adalah menemukan abstraksi untuk menangani event loop I/O
epoll()tanpa membuat orang ingin mencolok matanya sendiri. Sebelumnya ia juga mencoba banyak pendekatan lainasync/awaitsebenarnya dimulai bukan dari JavaScript, melainkan dari C#Anders Hejlsberg dari C# juga membuat TypeScript, dan fitur-fitur seperti class, arrow function, serta
async/awaitdi TypeScript pada akhirnya masuk ke ES6+Menurut saya itu solusi yang sangat baik untuk JS/TS yang merupakan event loop single-threaded. Namun semakin low-level sebuah bahasa, semakin buruk abstraksi ini, jadi sebagian besar kritik terhadap
asyncRust di sini validTulisan ini menjelaskan kompleksitas dan kesulitan
asyncRust dengan baik, tetapi penting juga diingat bahwa salah satu filosofi inti Rust adalah keamanan memori tanpa mengorbankan performaPola asinkron Rust, terutama cara membuat compiler menjamin keamanan data, menunjukkan filosofi ini dengan baik. Memang ada kompleksitas, tetapi nilainya ada pada model konkurensi yang lebih aman, yang memaksa developer memikirkan data dan alur eksekusi secara mendalam
Rust mungkin bukan jawaban untuk semua aplikasi userspace konkuren berskala besar, tetapi pada sistem yang mengutamakan ketangguhan dan keamanan, trade-off ini bisa dibenarkan. Seiring berkembangnya ekosistem, kemungkinan akan muncul lebih banyak abstraksi dan library yang mengurangi rasa sakit ini
Saya banyak menulis Rust lock-free berbasis
async. Masalah utamanya adalah Tokio future bersifat'static, dan ini berasal dari kesalahan desain yang tertanam jauh di ekosistem Rust, yaitu keputusan bahwa memory leak itu amanKarena itu, tidak ada jaminan statis bahwa future akan dibersihkan dengan benar. Saat sebuah task asinkron dibuat, jika seseorang melupakan future dengan
std::mem::forget, borrow checker tidak bisa tahu bahwa referensi yang diteruskan secara transitif oleh future itu masih hidupAlih-alih menaburkan
Arcke mana-mana, saya memakai crateunsafeseperti ini: https://docs.rs/async-scoped/latest/async_scoped/Ini menangkap 99% bug yang mungkin akan saya buat di C++, jadi merupakan kompromi yang masuk akal. Pekerjaan untuk mengimplementasikan future non-
'staticdengan cara yang aman juga sedang berjalan, semoga berhasilMasalah besar lainnya adalah
async traitsaat ini membutuhkan future yang di-boxing, sehingga menambahkanmalloc/freedi setiap batas pemanggilan fungsi, tetapi ini ada di roadmap perbaikan tahun iniSaran “pakai channel saja” juga membuat control flow tersebar ke mana-mana dalam codebase besar. Channel terasa seperti
GOTOmodern; saya juga memakainya, tetapi saya jarang menggunakannya saat hanya perlu menjalankan beberapa hal secara paralel lalu menunggu semuanya selesai'static, melainkan hanya future yang bisa di-spawnuntuk memanfaatkan konkurensi runtime yang harus'staticAgar bisa di-
poll(), future harus di-Pin, danT: !Unpinyang sudah di-Pinpada akhirnya harus memanggilDrop: https://doc.rust-lang.org/std/pin/#drop-guaranteeFuture yang dihasilkan oleh fitur
asynccompiler memiliki sifat ini, dan pada future manual punPhantomPinnedbisa ditambahkan. Berkat ini, setelah di-poll(), permainanmem::forgetdapat dianggap sebagai undefined behavior, dan library future yang intrusive maupun self-referential pun menjadi mungkin: https://docs.rs/futures-intrusive/latest/futures_intrusive/Future memang bisa tetap hidup dan bocor karena
Arc/Rc, tetapi dari sudut pandang developer library, hal itu tidak dapat dibedakan secara wajar dari penggunaan normal, atau tidak terlalu perlu dipedulikanRc, atau menambahkan batas traitunsafeyang menular