2 poin oleh GN⁺ 2023-09-09 | 1 komentar | Bagikan ke WhatsApp
  • async/await di 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 sebagai Send atau diperlakukan sebagai referensi 'static, dan karena sifat async yang menular, batasan seperti ini berulang di seluruh kode
  • Arc memang menyelesaikan masalah kompilasi, tetapi membuat lifetime objek dan resource menjadi kabur; lalu muncul jebakan lain seperti async rekursif, 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/await yang terlihat di C# atau Node.js
    • async fn tidak 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 Send agar 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::scope yang membatasi lifetime future menjadi lebih pendek daripada “selamanya”
  • async bersifat menular, sehingga fungsi yang memanggil fungsi async juga harus menjadi async
    • 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_on di 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 Arc secara 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 async rekursif menjadi tipe yang didefinisikan secara rekursif
    • Pengguna yang sekadar ingin memanggil dirinya sendiri harus melakukan boxing secara manual atau memakai crate seperti async-recursion
  • 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 async terasa 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 'static di 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

 
GN⁺ 2023-09-09
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/Vulkan
    Kalau 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 unsafe milik orang lain. Di kode saya, unsafe dilarang; memang sulit dikompilasi, tetapi setelah berhasil, cenderung “langsung berjalan”, jadi menurut saya jauh lebih baik daripada debugging konkurensi
    Ada juga keluhan. Rust kuat terhadap data race, tetapi tidak bisa mencegah deadlock, jadi diperlukan static analyzer yang melacak urutan lock di sepanjang jalur pemanggilan. async tidak 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 tanpa Rc dan Weak, dan sistem trait juga kompleks, sehingga pada bagian pemrosesan aset yang secara alami cocok dengan orientasi objek muncul kode duplikat
    Crate 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

    • Selama 3 tahun terakhir saya membuat simulator robot dengan Rust, dan pengalamannya hampir sama. Dalam 3 tahun, bug runtime nyata hanya sekitar 5, dan meskipun ada masalah pada Rust dan async, secara keseluruhan kelebihannya jauh lebih besar
    • Melacak urutan lock untuk menemukan potensi deadlock terdengar seperti ide bagus
      Seperti lockdep di 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 diimplementasikan
    • Saya melakukan hal yang hampir sama di MMO dengan Java, dan JDK membuatnya sangat mudah. Dari jaringan, buat model, pindahkan objek ke thread UI lewat concurrent queue; cukup sederhana sampai terasa membosankan, tetapi tetap cepat
    • Rust bukannya bebas dari race condition, melainkan bebas dari data race
      Di luar akses data, race condition masih bisa terjadi: https://news.ycombinator.com/item?id=23599598
    • Masalah prioritas relatif mudah dipecahkan
      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 aneh
    Jika memakai banyak Arc, RwLock, dan shared state, kodenya menjadi berantakan; dan memang benar kritik bahwa terutama ketika 'static mulai menyebar ke mana-mana, ia menginfeksi semuanya seperti function coloring. Dulu saya juga pernah kacau karena menempelkan Arc dan mencoba menangani lifetime borrowing dengan pintar
    Namun 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 Arc dan cara menangani lifetime. Kalau Anda mengimplementasikan runtime async, itu mungkin diperlukan, tetapi saya kurang paham mengapa pengguna library rata-rata harus begitu fokus pada hal ini

    • Kritiknya terasa agak aneh. async tidak otomatis berarti multithreading, dan jika async berjalan di thread yang sama, tidak ada sharing, sehingga tidak perlu menempelkan semua keyword ajaib pada hal yang dibagikan
      Saat 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 biasa
      Kekhawatiran soal Send+Sync juga kurang saya pahami. Berdasarkan pengalaman, kebanyakan hal dengan mudah Send+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 sesuai
    • Paradigma message passing memang sangat bagus, dan bahasa seperti Erlang menunjukkan bahwa itu pilihan hebat untuk sistem terdistribusi
      Namun cara coding seperti ini sangat berbeda dari JavaScript async yang 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 jalur Arc dan RwLock
    • Impian Smalltalk dan object-oriented yang sejati masih hidup
    • Saat kuliah saya mendapat nasihat seperti ini dari profesor, dan itu sangat membantu
      Menstrukturkan 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
    • Seperti kata seorang programmer bijak, “Jangan berkomunikasi dengan berbagi memori; berbagilah memori melalui komunikasi”
  • async pada 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 async terlalu 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, seperti tiny_http, rouille, dan astra.

    • Intinya adalah Rust salah mengimplementasikan coroutine.
      Dengan memilih coroutine tanpa stack, muncullah masalah async/await dan 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 async Rust tanpa runtime; mereka memakai Tokio, dan Tokio pada praktiknya melakukan hampir semua pekerjaan runtime yang tadinya ingin dihindari.
      Akibatnya, banyak pengguna async Rust mendapatkan sisi buruk dari kedua pendekatan. Di ranah embedded, ada juga yang memakai async Rust dengan runtime yang sangat tipis, tetapi jumlahnya sedikit dan bahkan mereka pun belum sepenuhnya yakin.
    • Saya melihat Tokio terseret lagi sebagai dependensi di program saya. Padahal saya tidak memakainya langsung; sebuah fungsi yang tidak saya pakai dari suatu crate membawa reqwest, lalu itu membawa h2, lalu itu membawa tokio lagi.
    • Saya penasaran apakah masih ada alasan memakai async jika 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.
  • async sudah menyebar ke terlalu banyak crate, sehingga seluruh program menjadi async atau setidaknya bergantung pada Tokio untuk banyak hal.
    Kalau ingin web server, kesannya pilihannya cuma async + tokio atau pergi saja, dan untuk konektor SQL pun suasananya seperti harus menulis sendiri kalau tidak mau yang asinkron. Setiap orang menyelesaikan masalah yang dibawa async dengan cara berbeda-beda, dan hal seperti closure async terasa 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.

    • Kalau ada primitif asinkron yang lebih baik di standard library atau crate 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 async Rust, tetapi setelah menulis beberapa ribu baris Rust sinkron bulan ini, yang saya rasakan adalah ketika rustc membuat 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. async masih 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 async belum 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 penasaran seperti apa “desain API asinkron yang baik” itu. Jika merancang server yang sepenuhnya berpusat pada asinkron, tetapi tetap scalable, mudah dipelihara, dan mudah dipahami, bentuknya seharusnya seperti apa?
      Saya juga memikirkan bagaimana mencegah asinkron menyebar ke seluruh codebase.
      Rancangan saya saat ini adalah struktur tempat thread I/O membagi event sistem dari liburing atau epoll ke dua tahap, “submit” dan “handle”, lalu mengirimkannya ke komponen lain. Misalnya jika membuat tcp-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 dengan EPOLLOUT/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} | state3
    • Kalau belum stabil, seharusnya juga tidak dipakai di produksi.
    • Menarik melihat komentar-komentar yang mengasumsikan penulisnya pemula Rust. Bisa jadi justru pengalaman penulis lebih banyak daripada mereka.
  • Masa pakai Arc bukan tidak bisa diketahui; ia ditentukan oleh di mana dan bagaimana ia dipegang
    Kesenjangan 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

    • Dalam arti itu, masa pakai objek dalam sistem garbage collection pun punya batas bawah, yaitu “selama masih direferensikan”
      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
    • Sebagian besar kritiknya terasa seperti itu. Saya kira ini akan menjadi tulisan tentang bagaimana transformasi async menghambat optimisasi yang bisa dilakukan compiler pada kode non-asinkron
      Bagian tentang bergulat dengan Weak tampaknya menunjukkan upaya membuat struktur ownership yang kompleks, dan itu memang tidak mudah di Rust secara umum. Saya sangat jarang memakai weak smart pointer
      Channel 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 Notify dan semaphore
      Mutex 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_HORROR
      Kritik 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_blocking
    • Reference counting juga merupakan salah satu jenis garbage collection https://en.wikipedia.org/wiki/Garbage_collection_(computer_s...
      Penulis kemungkinan besar tahu apa itu Arc dan bagaimana cara kerjanya; poinnya lebih dekat ke bahwa di Rust async, dibanding kode sinkron, Anda jauh lebih sering memakai Arc alih-alih RAII biasa
      Jika 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...
    • Masa pakai Arc bukan acak, melainkan tidak bisa diketahui secara statis
    • Arc di Rust bisa dipindahkan atau dipinjam, dan juga bisa digunakan tanpa menyentuh reference count
      Dalam banyak kasus, biayanya jauh lebih murah daripada objek dalam bahasa dengan reference counting implisit
  • Saya suka Rust, tetapi async itu berantakan, dan kita tidak bisa menulis kode asinkron seperti menulis kode sinkron
    Saya 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 tepat
    Saat ini saya sedang merangkai logika agar sebuah struct yang mengimplementasikan Future memanggil 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 library

    • Saya tidak setuju dengan bagian terakhir. async jelas menyakitkan juga bagi pengguna akhir, rasanya seperti memakai bahasa terpisah yang kehilangan fitur inti Rust seperti lifetime dan tipe eksplisit, lalu ditaburi banyak Pin
      Karena tidak bisa menjalankan fiber yang memiliki scope, akhirnya kita menempelkan banyak Arc; Pin sulit dipakai tanpa unsafe; dan perubahan sangat kecil pada fungsi asinkron bisa membuat future di seluruh codebase menjadi !Send
    • Pengembang library punya kapasitas lebih besar untuk menanggung kompleksitas dibanding pengguna. Menyerahkan pekerjaan seperti itu kepada developer berpengalaman yang membuat infrastruktur dasar adalah arah yang tepat
    • Saya pernah melihat salah satu VM wasm untuk Rust menyediakan sesuatu yang tampak seperti penjadwalan M:N transparan, dan pendekatan semacam itu sepertinya bisa menyelesaikan sebagian besar kesulitan async. Kita lihat saja bagaimana perkembangannya
  • Async Everything adalah bahasa yang buruk
    async/await adalah 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 lama
    Siapa 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/await juga bagus: https://www.youtube.com/watch?v=oNnITaBseYQ

    • Membenci JavaScript memang menyenangkan, tetapi menarik juga menonton ulang presentasi Ryan Dahl saat pertama kali memperkenalkan Node.js: https://www.youtube.com/watch?v=EeYvFl7li9E
      Ia 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 lain
    • async/await sebenarnya dimulai bukan dari JavaScript, melainkan dari C#
      Anders Hejlsberg dari C# juga membuat TypeScript, dan fitur-fitur seperti class, arrow function, serta async/await di 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 async Rust di sini valid
  • Tulisan ini menjelaskan kompleksitas dan kesulitan async Rust dengan baik, tetapi penting juga diingat bahwa salah satu filosofi inti Rust adalah keamanan memori tanpa mengorbankan performa
    Pola 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 aman
    Karena 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 hidup
    Alih-alih menaburkan Arc ke mana-mana, saya memakai crate unsafe seperti 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-'static dengan cara yang aman juga sedang berjalan, semoga berhasil
    Masalah besar lainnya adalah async trait saat ini membutuhkan future yang di-boxing, sehingga menambahkan malloc/free di setiap batas pemanggilan fungsi, tetapi ini ada di roadmap perbaikan tahun ini
    Saran “pakai channel saja” juga membuat control flow tersebar ke mana-mana dalam codebase besar. Channel terasa seperti GOTO modern; saya juga memakainya, tetapi saya jarang menggunakannya saat hanya perlu menjalankan beberapa hal secara paralel lalu menunggu semuanya selesai

    • Pembedaan yang penting adalah bukan Tokio future itu sendiri yang bersifat 'static, melainkan hanya future yang bisa di-spawn untuk memanfaatkan konkurensi runtime yang harus 'static
      Agar bisa di-poll(), future harus di-Pin, dan T: !Unpin yang sudah di-Pin pada akhirnya harus memanggil Drop: https://doc.rust-lang.org/std/pin/#drop-guarantee
      Future yang dihasilkan oleh fitur async compiler memiliki sifat ini, dan pada future manual pun PhantomPinned bisa ditambahkan. Berkat ini, setelah di-poll(), permainan mem::forget dapat 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 dipedulikan
    • Jika menganggap keamanan memory leak sebagai kesalahan desain, saya penasaran apakah Anda lebih memilih menghilangkan interior mutability, menghilangkan Rc, atau menambahkan batas trait unsafe yang menular