Async Rust tidak pernah benar-benar keluar dari status MVP
(tweedegolf.nl)- Async Rust memungkinkan kode yang independen dari executor berjalan baik di server maupun mikrokontroler, tetapi state machine yang dibuat compiler menyebabkan pembengkakan ukuran biner yang sangat terlihat, terutama di embedded
- Bahkan contoh sederhana seperti
bar()dengan 2 titikawaitmenghasilkan 360 baris MIR serta stateUnresumed,Returned,Panicked,Suspend0,Suspend1, sedangkan versi sinkron hanya membutuhkan 23 baris - Jika future yang sudah selesai di-
polllagi diubah agar mengembalikanPoll::Pendingalih-alihpanic, kontrak tetap bisa dipenuhi tanpa perilaku unsafe, dan eksperimen menunjukkan ukuran biner firmware embedded berkurang 2%~5% - Bahkan
async { 5 }tanpaawaitsaat ini tetap membuat state machine dengan 3 state dasar, tetapi jika dioptimalkan agar selalu mengembalikanPoll::Ready(5), ukuran biner embedded berkurang 0.2% - Project Goal yang diusulkan bertujuan mendorong penghapusan panic setelah selesai di mode rilis, penghapusan state machine untuk async block tanpa await, inline future dengan satu await, dan penggabungan state yang identik di compiler
Masalah pembengkakan di level compiler pada Async Rust
- Async Rust memungkinkan kode yang independen dari executor berjalan sekaligus di server dan mikrokontroler, tetapi pada mikrokontroler kecil peningkatan ukuran biner sangat terasa
- Blog Rust memperkenalkan async/await sebagai abstraksi tanpa biaya, tetapi pada praktiknya async menghasilkan banyak pembengkakan (bloat), dan meski masalah yang sama juga ada di desktop dan server, hal itu kurang terlihat karena memori dan sumber daya komputasinya lebih besar
- Setelah ada cara mengakali agar tidak terlalu membengkak saat menulis kode async, kini diajukan Project Goal untuk menyelesaikan masalah ini di compiler
- Masalah future yang menjadi terlalu besar dan terlalu banyak disalin tidak termasuk dalam cakupan
- Masalah ini sudah diketahui, dan sudah ada PR yang menangani sebagian darinya: https://github.com/rust-lang/rust/pull/135527
Struktur future yang dihasilkan
- Kode contoh memperlihatkan
foo()mengembalikanasync { 5 }, danbar()menjalankanfoo().await + foo().await- Contoh Godbolt: godbolt
barmemiliki 2 titik await, sehingga state machine minimal membutuhkan 2 state, tetapi pada kenyataannya lebih banyak state yang dibuat- Compiler Rust dapat membuang MIR di berbagai pass, dan pass
coroutine_resumeadalah pass MIR khusus async terakhir- Async masih tersisa di MIR tetapi tidak lagi ada di LLVM IR, jadi proses perubahan async menjadi state machine terjadi di pass MIR
- Fungsi
barmenghasilkan 360 baris MIR, sedangkan versi sinkron hanya memakai 23 baris CoroutineLayoutyang dikeluarkan compiler pada dasarnya adalah kumpulan state berbentuk enumUnresumed: state awalReturned: state selesaiPanicked: state setelah panicSuspend0: titik await pertama, menyimpan futurefooSuspend1: titik await kedua, menyimpan hasil pertama dan futurefookedua
Future::polladalah fungsi yang aman, sehingga memanggilnya lagi setelah future selesai tidak boleh menyebabkan UB- Saat ini setelah
Suspend1, ia mengembalikanReadylalu mengubah future ke stateReturned - Jika di-
polllagi pada state ini, panic akan terjadi
- Saat ini setelah
- State
Panickedtampaknya ada agar setelah fungsi async panic dan ditangkap olehcatch_unwind, future tersebut tidak bisa di-polllagi- Setelah panic, future bisa berada dalam state yang tidak lengkap, sehingga mem-
polllagi dapat berujung pada UB - Mekanisme ini sangat mirip dengan mutex poisoning
- Interpretasi terhadap state
Panickedini tidak didukung dokumentasi yang benar-benar pasti, jadi tingkat keyakinannya sekitar 90%
- Setelah panic, future bisa berada dalam state yang tidak lengkap, sehingga mem-
Apakah poll setelah selesai memang harus panic
- Future dalam state
Returnedsaat ini akan panic, tetapi sebenarnya itu tidak wajib- Syarat yang penting hanyalah tidak menimbulkan UB
- Panic relatif mahal dan menambahkan jalur dengan efek samping yang sulit dihilangkan lewat optimasi
- Jika future yang sudah selesai di-
polllagi mengembalikanPoll::Pending, kontrak tipeFuturetetap terpenuhi tanpa perilaku unsafe - Saat compiler dimodifikasi untuk menguji pendekatan ini, firmware embedded async menunjukkan penurunan ukuran biner 2%~5%
- Perilaku ini diusulkan tersedia sebagai sakelar seperti
overflow-checks = falsepada integer overflow- Di build debug, panic tetap dipertahankan agar perilaku yang salah langsung terlihat
- Di build rilis, future yang lebih kecil bisa didapatkan
- Saat menggunakan
panic=abort, ada kemungkinan statePanickeditu sendiri dapat dihapus, tetapi dampaknya masih perlu ditinjau lebih lanjut
State machine tetap dibuat meski tidak ada await
foo()hanya mengembalikanasync { 5 }, sehingga bentuk implementasi manual yang paling optimal adalah future tanpa state yang selalu mengembalikanPoll::Ready(5)- Namun MIR yang dihasilkan compiler masih memiliki 3 state dasar:
Unresumed,Returned,Panicked- Saat di-
poll, discriminant dari state saat ini diperiksa lalu bercabang - Jika di-
polllagi setelah selesai, panic terjadi dengan assert`async fn` resumed after completion
- Saat di-
- Dalam kasus ini, compiler bisa dioptimalkan agar tidak membuat state machine dan cukup selalu mengembalikan
Poll::Ready(5) - Saat ini diterapkan secara eksperimental di compiler, ukuran biner embedded berkurang 0.2%
- Penghematannya memang tidak besar, tetapi optimasinya sederhana sehingga tetap berpotensi layak diterapkan
- Optimasi ini sedikit mengubah perilaku, tetapi yang terdampak hanyalah executor yang tidak mematuhi kontrak
- Compiler saat ini panic pada poll berikutnya
- Setelah optimasi, future akan selalu mengembalikan
Ready
LLVM saja tidak cukup
- Meski keluaran MIR tidak efisien, kadang LLVM bisa membereskannya, tetapi syaratnya terbatas
- Future harus cukup sederhana
- Harus memakai
opt-level=3
- Saat future menjadi lebih kompleks, LLVM tidak mampu menghapus semuanya, dan dalam kode Async Rust yang idiomatis future cenderung bertingkat dalam, sehingga kompleksitas cepat membesar
- Di lingkungan seperti embedded atau wasm yang sering mengutamakan optimasi ukuran, LLVM tidak bisa mengoptimalkan semuanya
- Contoh Godbolt: https://godbolt.org/z/58ahb3nne
- Dari assembly yang dihasilkan, LLVM mengetahui bahwa
foomengembalikan 5, tetapi tidak bisa mengoptimalkan jawabanbarmenjadi 10 - Panggilan ke fungsi poll milik
foojuga masih tersisa - Penyebabnya adalah adanya jalur panic potensial yang tidak bisa dipahami compiler sepenuhnya
- LLVM tidak tahu bahwa
foopada kenyataannya hanya dipanggil sekali dan tidak panic
- Dari assembly yang dihasilkan, LLVM mengetahui bahwa
- Jika cabang panic di IR dikomentari, optimasinya menjadi lebih baik: https://godbolt.org/z/38KqjsY8E
- Daripada berharap LLVM melakukan optimasi lanjutan setelahnya, compiler seharusnya memberi input yang lebih baik ke LLVM
Inline future tidak berjalan dengan baik
- Inline penting karena memungkinkan pass optimasi berikutnya, tetapi future Rust yang dihasilkan saat ini tidak di-inline pada tahap awal
- Setelah tiap future mendapat implementasinya, LLVM dan linker memang mendapat kesempatan untuk melakukan inline, tetapi akibat masalah sebelumnya tahap itu sudah terlambat
- Peluang inline paling langsung adalah ketika
bar()hanya melakukanfoo(blah).await- Pola ini sering muncul saat membuat abstraksi dengan trait
- Compiler saat ini membuat state machine untuk
barlalu memanggil state machinefoodi dalamnya - Secara lebih efisien,
barbisa langsung menjadi futurefooitu sendiri
- Jika ada preamble dan postamble, kasusnya lebih kompleks
- Contoh:
bar(input)membuatblahdariinput > 10, lalu menjalankanfoo(blah).awaitdan menerapkan* 2pada hasilnya - Ini umum saat mengubah fungsi async ke signature lain, terutama dalam implementasi trait
- Contoh:
- Bentuk
barseperti ini juga tidak memerlukan state async miliknya sendiri- Tidak ada data yang perlu dipertahankan melewati satu titik await selain nilai yang sudah ditangkap oleh
foo - Namun
bartidak bisa begitu saja menjadifooitu sendiri, meski sebagian besar state dapat bergantung padafoo
- Tidak ada data yang perlu dipertahankan melewati satu titik await selain nilai yang sudah ditangkap oleh
- Dalam implementasi manual,
BarFutdapat memiliki stateUnresumed { input }danInlined { foo: FooFut }- Pada poll pertama, preamble dijalankan untuk membuat
foo(blah)lalu diubah ke stateInlined - Setelah itu, hasil
foo.poll(cx)diteruskan dengan menerapkan postamble
- Pada poll pertama, preamble dijalankan untuk membuat
- Jika kode hingga sebelum titik await pertama bisa dieksekusi lebih awal, state
Unresumedjuga bisa dihapus, tetapi hal itu tidak bisa diubah karena ada jaminan bahwa future tidak melakukan apa pun sebelum di-poll - Jika properti future yang sedang di-
pollbisa diinspeksi, optimasi inline tambahan dapat dimungkinkan- Misalnya, jika diketahui bahwa future selalu mengembalikan ready pada poll pertama, future pemanggil tidak perlu membuat state untuk titik await tersebut
- Jika optimasi semacam ini diterapkan secara rekursif, banyak future bisa dilipat menjadi state machine yang jauh lebih sederhana
- Dalam struktur
rustcsaat ini, tampaknya tiap async block ditransformasikan secara terpisah dan data terkait tidak dipertahankan setelahnya, sehingga kueri seperti ini tidak memungkinkan - Inline future belum diuji secara eksperimental, tetapi diperkirakan sangat membantu ukuran biner dan performa
Menggabungkan state yang identik
- Setiap titik await dalam async block menambah state baru pada state machine
- Kode seperti berikut terasa alami, tetapi karena kedua cabang me-
awaitfungsi async yang sama, akan muncul 2 state identikCommandId::A => send_response(123).awaitCommandId::B => send_response(456).await
- Dalam kasus ini,
CoroutineLayoutmemiliki_s0dan_s1yang masing-masing menyimpan tipe coroutinesend_responseyang sama, serta membuat 2 state:Suspend0danSuspend1 - MIR fungsi ini memiliki 456 baris, dan banyak basic block pada dasarnya duplikat
- Jika kode direfaktor manual agar lebih dulu menghitung nilai respons lalu hanya sekali memanggil
send_response(response).await, state yang duplikat hilangCommandId::Amenjadi123CommandId::Bmenjadi456- Setelah itu
send_response(response).await
- Setelah refaktor,
CoroutineLayouthanya memiliki satu future yang disimpan dan hanya menyisakan satu stateSuspend0 - Panjang total MIR turun menjadi 302 baris, dan duplikasi hilang
- Karena itu, pass optimasi untuk menemukan jalur kode dan state yang identik lalu menggabungkannya tampak berguna
- Optimasi ini kemungkinan akan cocok digabungkan dengan pass inline future
Tautan eksperimen dan benchmark tambahan
- Jika kedua eksperimen diterapkan bersama, benchmark sintetis x86 dengan executor
smolmenunjukkan peningkatan performa sekitar 3% - No panics in poll after ready: https://github.com/rust-lang/rust/compare/main...diondokter:rust:resume-pending
- No await, no statemachine: https://github.com/rust-lang/rust/compare/main...diondokter:rust:no-statemachine-when-no-await
Permintaan dukungan untuk Project Goal
- Pekerjaan ini diajukan sebagai Project Goal agar bisa dikerjakan di compiler: https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html
- Tanpa pendanaan, sulit untuk mendorong banyak pekerjaan, sehingga dibutuhkan dukungan parsial atau penuh dari perusahaan atau organisasi yang akan mendapat manfaat dari pekerjaan ini
- Kontaknya adalah
dion@tweedegolf.com - Cakupan pekerjaan dan besaran pendanaan yang dibutuhkan bersifat fleksibel, tetapi diperkirakan €30k cukup untuk menyelesaikan seluruh atau sebagian besar pekerjaan
2 komentar
Komentar Hacker News
Saya setuju judulnya agak berlebihan, tapi isi tulisannya bagus dan poin utamanya tersampaikan dengan baik
Saya belum cukup berpengalaman untuk punya pendapat kuat soal async Rust, tapi ada beberapa hal yang menonjol
Hal baiknya adalah kita bisa punya runtime yang eksplisit. Alih-alih mencemari seluruh proyek menjadi async, default-nya bisa tetap sinkron dan runtime hanya dipakai di “batas” I/O
Untuk proyek yang sedang saya kerjakan, pendekatan ini cocok, dan tampaknya cukup mirip dengan strategi yang diambil Zig untuk kode I/O. Dalam kasus ini, masalah warna fungsi juga sebagian besar teratasi, dan karena kami memang harus memisahkan dengan ketat kode I/O dan kode yang berpusat pada CPU, runtime I/O yang eksplisit terasa alami
Hal buruknya adalah seluruh ekosistem tampak terlalu bergantung pada tokio. Mirip seperti GC di Java itu opsional tetapi pada praktiknya semua orang memakai runtime GC pihak ketiga yang sama, dan library apa pun yang Anda ambil memaksa runtime itu. Ketergantungan yang terpusat seperti ini tidak sehat
Kebutuhan runtime async pada prosesor workstation dan pada lingkungan seperti RP2040 sangat berbeda. Meski begitu, karena backend bisa diganti, saat menulis kode I/O async untuk mikrokontroler ARM M0 kecil, dengan memakai embassy sebagai runtime yang berfokus pada embedded, tampilannya tetap hampir sama dengan kode yang dipakai di lingkungan lain
Karena memakai trait dan antarmuka yang sama, kita bisa lebih sedikit memikirkan detail runtime. Dibanding memakai RTOS kecil atau membangun lingkungan async sendiri, ini cukup bagus
Hal-hal yang dipelajari saat menulis kode async di embassy juga bisa dibawa ke area lain
Walaupun tokio bukan bagian dari standard library, ia dikelola dengan baik, jadi keadaan sekarang tampak baik-baik saja. Justru saya khawatir kalau masuk ke standard library, akan lebih sulit memakai executor lain, dan standard library juga bisa jadi lebih sulit dipindahkan ke platform lain
Tentu saja, bisa jadi kekhawatiran ini tidak berdasar
Logging sekarang kurang lebih sudah tersatukan dengan slf4j, tetapi masih ada library yang memakai hal lain, dan utilitas umum dulu Apache Commons lalu sekarang banyak yang memakai Guava
Untuk JSON, sebagian besar terkonsolidasi ke Jackson, tetapi Gson dan Simple-json juga masih umum, dan anotasi nullability juga belum pernah benar-benar diresmikan, bergerak dari distribusi tidak resmi JSR-305 ke checker framework, lalu belakangan ke JSpecify
Elemen-elemen dasar seperti ini seharusnya disediakan oleh bahasa agar tidak terjadi fragmentasi dan menjamurnya standard library de facto
Menulis library agar independen dari executor tidak terlalu sulit, tetapi butuh perhatian terus-menerus, dan itu bukan sesuatu yang selalu dijaga oleh sebagian besar komunitas
Tulisan yang luar biasa. Saya suka analisis optimisasi mendalam seperti ini, dan semoga tujuan proyeknya juga berjalan baik
Saya kadang merasa compiler sering tidak mengerahkan banyak usaha untuk optimisasi kasus yang “sepele”
Namun, judulnya terlalu dramatis dibanding isinya. Saya rasa saya tetap akan mengklik kalau judulnya “Async Rust Optimizations the Compiler Still Misses”
Sekarang async bisa dipakai di trait dan closure, tetapi itu pembaruan sistem tipe, bukan perubahan pada mesin async itu sendiri. Waker juga sedikit lebih mudah ditangani, tetapi itu lebih dekat ke perbaikan di sisi std/core
Setahu saya, orang-orang yang mendaratkan async Rust mengalami burnout yang cukup berat dan aktivitas mereka berkurang, dan hampir tidak ada orang yang melanjutkan pekerjaan itu. Meski begitu, cukup menyenangkan melihat orang-orang dari Google membuka sebuah PR untuk mengoptimalkan tata letak memori variabel yang ditangkap
Saya dan rekan kerja banyak memakai async, jadi mungkin kami sendiri harus mengerjakannya, atau setidaknya memulainya. “Gratis” di sini tampaknya lebih mirip gratis seperti memelihara anak anjing
Jadi ya, judulnya memang sedikit clickbait, tetapi saya tetap tidak berniat menariknya kembali
Penulis tampak terlalu terobsesi dengan overhead fungsi kecil. Ia terganggu oleh overhead status “panic” dan “returned”, padahal itu bukan masalah besar
Sebagian besar blok async yang berguna cukup besar sehingga overhead kasus error tertutupi
Soal kurangnya inlining, mungkin memang ada benarnya. Tetapi yang biasanya membatasi jumlah aktivitas dalam skala besar adalah ruang state yang dibutuhkan tiap aktivitas
Secara umum async tampak seperti ide yang belum matang. Kode biasa pun sebenarnya sudah asinkron
Jika Anda harus menunggu pekerjaan async, thread akan tidur sampai siap dan kernel mengabstraksikannya untuk Anda. Lalu karena orang tidak suka menyusun kode sebagai thread logis, mereka menambahkan sistem callback untuk event, dan setelah itu sadar bahwa callback sulit dipahami serta kontrol berurutan lebih baik
Jadi saya melihat thread adalah model pemrograman yang benar
Sekarang runtime bahasa lebih menyukai “green thread” demi portabilitas dan performa, tetapi kebanyakan bahasa tidak benar-benar menyediakannya dengan baik. Sebagai gantinya, muncul masalah warna async/non-async, scheduling, prioritas, dan non-preemption. Model scheduling dan proses yang lebih buruk daripada tahun 1970-an
Kode async pun sering ditulis dengan cara yang gagal memaksimalkan konkurensi yang dapat diekspresikan. Misalnya bukan “jalankan N pekerjaan I/O sekaligus”, tetapi ditulis seperti “untuk setiap pekerjaan X, await process(x)”
Namun di dunia thread, masalah konkurensi ini justru lebih parah. Thread pada dasarnya terlalu berat untuk mengekspresikan konkurensi secara efisien, dan tidak ada cara untuk mengoptimalkannya ke arah itu
Ini bukan pelajaran baru. Sudah lama diketahui bahwa executor work-stealing punya latensi jauh lebih rendah dan P99 yang lebih konsisten daripada thread tradisional. Itulah alasan Apple membuat GCD pada awal 2000-an
Thread tidak memberi scheduler kernel informasi yang cukup kaya untuk memahami beban kerja, dan thread kernel adalah mekanisme yang terlalu berat untuk mendapatkan konkurensi yang halus. Untuk beban yang bukan murni komputasi, melainkan I/O atau campuran, hasilnya malah lebih buruk
Tidak semua program butuh tingkat performa seperti ini, tetapi dengan usaha yang sama jauh lebih mudah mencapai standar performa yang lebih tinggi, dan pada praktiknya Anda bisa mendapatkan latensi dan throughput yang sulit dikejar pendekatan tradisional
Tanda bahwa arah async itu benar juga terlihat dari io_uring. Pendekatan I/O performa tinggi di kernel melalui io_uring benar-benar berbeda dari threading dan system call tradisional, dan penanganan completion-nya jauh lebih dekat dengan konkurensi async. Hanya saja async/await saja tidak punya cukup “warna” untuk mengekspresikan hubungan antar tugas async, sehingga lebih sulit dimanfaatkan sepenuhnya
Terakhir kali saya mengutak-atik kode coroutine/scheduling, membuat thread yang langsung selesai lalu melakukan join memakan sekitar 200µs, sedangkan membuat green thread sendiri, menjadwalkannya, lalu menunggunya hanya sekitar 400ns
Tidak perlu menunggu 10 tahun sampai seseorang lagi-lagi merancang framework async yang absurdly rumit. Di bahasa sistem mana pun, dengan 20 baris assembly Anda bisa langsung membuat green thread/coroutine bertumpuk sendiri
Optimisasi kode yang berpusat pada bandwidth adalah masalah desain jadwal. Dalam model multithreading klasik, kontrol atas scheduling terbatas, tetapi dalam model async, kontrolnya hampir sempurna
Jadwal async yang dioptimalkan dengan baik jauh lebih cepat daripada arsitektur multithread setara untuk pekerjaan berpusat pada bandwidth yang sama, sampai tidak layak dibandingkan
Sebagian besar kode performa tinggi saat ini berpusat pada bandwidth, dan async ada untuk membuat beban kerja seperti ini lebih mudah dioptimalkan
Saat menguji pemrosesan konkuren dan memastikan race condition ditangani dengan benar, callback jauh lebih mudah karena Anda bisa mengendalikan scheduling. Karena tiap callback merepresentasikan unit terpisah, Anda bisa melihat event mana yang dapat diurutkan ulang, dan lebih mudah meninjau berbagai urutan
Sebaliknya, pada thread orang mudah mengabaikan urutan, dan tidak memikirkan kapan kompleksitas dari thread lain bisa memengaruhi thread saat ini. Itu bukan sederhana, melainkan lebih seperti penyederhanaan
Selain itu, tanpa menambahkan penghalang buatan untuk menghentikan thread, atau tanpa mengganti I/O dengan stub dan meneruskan mock yang memiliki callback untuk mengontrol urutan, sulit benar-benar mengubah dan menguji skenario konkurensi
Masalah callback adalah call stack yang tertangkap bukan call stack logis. Kecuali ada library/runtime tertentu yang berusaha membuat call stack bermakna, Anda membutuhkan definisi error yang baik
Tentu saja, Anda juga bisa mencampur dua paradigma ini dan hanya mendapatkan kelemahan keduanya
Jika tujuan utama Rust adalah keamanan, saya tidak paham kenapa ada panic. Seharusnya kita bisa membuktikan bahwa tidak ada jalur yang mungkin panic dalam kode
Saya memeriksanya sepanjang minggu ini, dan ternyata sangat sulit membuat program yang dijamin tidak pernah panic. Setahu saya panic handler ukurannya sekitar 300KB, dan satu-satunya cara untuk mengecualikannya adalah kode Anda memang sama sekali tidak punya jalur yang bisa panic saat kompilasi. Mengecek apakah panic handler ikut masuk ke biner setelah kompilasi terasa seperti hack
Anda memang bisa melarang unwrap dan operasi panic lain lewat lint, tetapi andai ada subset Rust tanpa panic, banyak masalah yang dibahas tulisan ini akan hilang
Sangat menjengkelkan berurusan dengan bahasa yang punya begitu banyak operasi yang secara teoretis bisa panic, padahal pada praktiknya itu hanya mungkin terjadi pada hal setingkat bit flip, sama seperti saat membuktikan array tidak kosong atau saat menangani async
Akhirnya Anda harus menambahkan banyak sekali penanganan error untuk situasi yang tidak akan pernah benar-benar terjadi, atau memakai struktur aneh seperti pola list tak-kosong dengan field pertama dan sisa list terpisah. Dan struktur itu sendiri juga menambah bloat-nya sendiri
Pekerjaan untuk memperluas penggunaan berbasis pembuktian, termasuk pembuktian bahwa array tidak kosong, juga sedang berjalan pelan-pelan
Jika tidak ada panic dan semua situasi harus terus dieksekusi, maka untuk situasi seperti korupsi memori yang merusak invariant, Anda perlu menambahkan banyak penanganan error di semua tempat yang memeriksa invariant demi mencoba pemulihan
Itu justru jenis masalah yang sama persis dengan yang Anda khawatirkan, yaitu penanganan error besar-besaran untuk situasi yang nyaris tidak pernah terjadi
Saya cukup lelah dengan sikap yang menginginkan alat membuat semuanya mustahil gagal tanpa orang itu sendiri mau melakukan apa pun. Mereka ingin API yang mudah, lalu kalau itu belum cukup mudah mereka ingin container Kubernetes yang “diprogram” dengan YAML, dan kalau itu pun belum cukup mudah mereka ingin layanan hosting klik-klik dari GCP atau Amazon
Pada akhirnya itu terasa lebih seperti ingin mengonsumsi aplikasi yang tidak pernah gagal, bukan ingin memrogram, dan gaya hidup seperti itu hanya bisa ada di atas hubungan simbiosis dengan orang-orang yang benar-benar membangun sesuatu
Diskusi seperti ini, jelek tetapi perlu, juga sudah berlangsung cukup lama di C++
Sejak async diperkenalkan di Rust, saya memang tidak suka sifatnya yang menular
Saya berharap Rust berhasil, dan kalau lebih banyak orang seperti ini muncul, masa depan Rust bisa jadi lebih cerah
Saya baru mulai mengerjakan async Rust belakangan ini, dan masalah utama yang saya rasakan sekarang adalah duplikasi kode
Setiap fungsi yang ingin mendukung API async dan API blocking harus ditulis dua kali. Rasanya akan bagus kalau ada
maybe-asyncSaya mencoba mengakalinya dengan melihat crate seperti maybe-async dan bisync, tetapi semuanya punya masalah atau batasan yang berat
asyncatauconstUntuk saat ini, pilihan terbaik untuk menulis kode yang ingin hidup di dunia sinkron maupun asinkron adalah sans-io. Thomas Eizinger dari Fireguard menulis artikel yang bagus tentang pola ini[1]
Pola ini bukan hanya menyelesaikan masalah sync/async dengan rapi, tetapi juga mempermudah testing, dan membuka jalan ke teknik seperti DST[2]
Saya juga menulis artikel tentang topik ini[3], dan menekankan bahwa masalahnya lebih luas daripada async versus sync, karena juga mencakup executor yang berbeda-beda
0: https://github.com/rust-lang/effects-initiative
1: https://www.firezone.dev/blog/sans-io
2: https://notes.eatonphil.com/2024-08-20-deterministic-simulat...
3: https://hugotunius.se/2024/03/08/on-async-rust.html
asyncsudah merupakanmaybe-asyncPerbedaan antara
fn -> voiddanfn -> Futureadalah yang pertama langsung berjalan sampai selesai, sedangkan yang kedua mungkin baru selesai nantiJika Anda ingin menjalankan fungsi async secara blocking, cukup pakai executor blocking
Saya suka tulisan ini karena membuat saya juga melihat tujuan Rust 2026
Tim kami memakai Rust, tetapi kami belum perlu masuk terlalu dalam untuk melakukan pekerjaan yang kami butuhkan. Meski begitu, menyenangkan melihat bahasa dengan banyak umpan balik komunitas berkembang dari bawah
Di C++ saya tidak terlalu merasakan alur seperti ini, dan di area lain saya juga tidak terlalu tahu bagaimana prosesnya berjalan
Tapi satu hal yang agak disayangkan adalah tiap tujuan tampaknya membutuhkan pendanaan khusus, jadi agak terasa seperti Kickstarter. Saya penasaran apakah ini model terbaik yang sudah ditemukan sejauh ini
Tujuan proyek adalah sistem di mana satu orang atau kelompok kecil menyatakan ingin mengerjakan sesuatu, lalu meminta relawan proyek Rust menyediakan waktu dukungan berkelanjutan seperti code review atau menjawab pertanyaan
Itu tidak berarti proyek Rust sendiri yang menetapkan tujuan tersebut, atau pasti mendukungnya
Jadi kurang tepat melihatnya sebagai roadmap resmi Rust; lebih tepat dipahami sebagai “ada kontributor yang ingin bekerja di area ini”
Kalau teknologi sudah mapan secara komersial, sayangnya hal seperti ini memang sering terjadi. Sulit juga menyalahkan sponsor besar karena hanya mendanai area yang mereka pedulikan
Untungnya, setahu saya sebagian besar pendanaan TweedeGolf berasal dari pemerintah Belanda
Fitur baru bisa “dijual”. Membangunnya memang butuh uang, tetapi ia menyelesaikan masalah nyata, dan jika biaya masalah itu lebih besar daripada biaya pengembangan fitur, perusahaan biasanya bersedia membayar
Pemeliharaan lebih sulit, tetapi sekarang sudah ada juga dana untuk maintainer. Contohnya adalah dana RustNL: https://rustnl.org/maintainers/
Dana seperti ini menargetkan pekerjaan yang lebih luas dan berkelanjutan, dengan banyak organisasi menyumbang sedikit demi sedikit untuk menopangnya
Saya tidak tahu apakah itu model terbaik, tetapi setidaknya tampaknya cukup bekerja
Jika membaca dokumentasi Rust Async dan Tokio, sebenarnya sudah dijelaskan dengan baik kenapa bagian yang intensif CPU tidak boleh dimasukkan ke stack async, cara memakai alat dasar seperti
std::sync::Mutexsecara efisien di dalam blok async, dan cara menyambungkan kode sinkron dengan kode asyncBanyak kode tidak mengikuti panduan ini karena tidak peduli atau tidak membutuhkan efisiensi. Tetapi ada banyak proyek yang memang mementingkan performa dan efisiensi, dan begitu kodenya berjalan di produksi, jebakannya mulai terlihat. ScyllaDB adalah salah satu contohnya
LLM juga tidak membantu. Ia menjadikan semuanya async sampai
main, memakai alat dasar yang salah, dan tidak merancang sistem dengan benarMelipat state yang duplikat, yaitu pola menarik
matchke luar cabang await seperti pada contohprocess_command, adalah cara termudah yang bisa diterapkan siapa pun pada kode async yang ada hari iniTidak butuh pekerjaan compiler, cukup refactoring
Soal bagian “Future tidak mudah di-inline”, di bahasa pemrograman yang saya buat sendiri saya menulis custom pass untuk meng-inline pemanggilan fungsi async di dalam fungsi async
Secara umum hasilnya baik dan bisa menghilangkan sebagian boilerplate, tetapi ukuran biner hasilnya membengkak cukup banyak
Secara teknis Rust juga bisa melakukan hal yang sama
Komentar Lobste.rs
Ternyata ini tulisan yang jauh lebih konstruktif daripada yang saya perkirakan hanya dari judulnya
Saya berharap siapa pun yang ingin mengerjakan ini mendapat dukungan yang dibutuhkan
Senang melihat masalah ini sedang ditangani. Saya sudah beberapa kali melihat tulisan bahwa saat ini rustc mengirim terlalu banyak kode ke LLVM dan berharap optimizer menangani semuanya, dan khususnya tulisan ini juga meminta pendanaan untuk pekerjaan tersebut
Astaga, saya yang bodoh
Saya selalu mengira async pada dasarnya pasti “gemuk” karena bagaimanapun juga ia memerlukan runtime, pelacakan task, dan polling untuk memeriksa penyelesaian. Overhead itu kan memang tidak nol
Saya menganggap “abstraksi tanpa biaya” yang dibicarakan di sini merujuk pada fitur bahasa, terpisah dari runtime tambahan yang menyertainya
Saya bahkan tidak pernah terpikir untuk melihat apa yang sebenarnya dikeluarkan rustc sebelum diserahkan ke LLVM
Untuk orang yang belum akrab dengan async Rust:
Ini memang benar sekali. Bahkan pohon panggilan async yang bersarang, setelah dioptimalkan semaksimal mungkin, akan mengeras menjadi satu struct tunggal dengan state machine di dalamnya. Benar-benar pendekatan yang cerdas
Apakah jika mencapai kasus ini dalam build rilis akan muncul semacam deadlock? Atau mungkinkah terjadi kebocoran karena task-task menunggu pekerjaan yang selalu
Pending?Dengan
.await, polling yang keliru tidak mungkin terjadiAda beberapa hal yang terpikir:
panic=unwind. Selain beberapa test harness, saya hampir tidak pernah melihat keunggulan dibandingpanic=abortyang cukup untuk menutup biayanya. Bahkan untuk test harness pun, di Linux sepertinya mungkin menerapkan pilihan serupa dengan menggunakanclonesecara agak aneh untukwaitthread eksekusi alih-alihpthread_join. Bisa jadi saya salah soal iniApakah tautannya baru saja mati juga buat orang lain?
Sunting: tulisan blog muncul sekitar setengah detik lalu pindah ke halaman 404
Sunting 2: saya masuk ke daftar tulisan blog, mengklik beberapa hal, dan bahkan saat membuka tulisan itu dari daftar tetap menuju halaman 404. Bagaimana bisa blog yang statis, atau setidaknya seharusnya statis, dibuat rusak seperti ini?
Sebagai catatan, saya mengikuti langkah reproduksi yang tampaknya sama dan sama sekali tidak mendapatkan 404. Saya mencobanya di ponsel dan desktop, dengan JavaScript aktif maupun nonaktif. Jadi sepertinya gejala yang dialami mungkin lebih rumit daripada yang terlihat