4 poin oleh GN⁺ 2024-03-26 | 1 komentar | Bagikan ke WhatsApp
  • async/await di Rust bukan sekadar pengganti thread, melainkan model pemrograman yang mengekspresikan kode konkurensi berpusat I/O sebagai state machine yang dapat dikomposisi
  • Kode yang harus menangani banyak koneksi secara bersamaan, seperti web server, akan cepat terbatas jika hanya mengandalkan eksekusi linear; thread memungkinkan pemrosesan konkuren dengan memisahkan penanganan klien lewat thread::spawn
  • async/await menyerahkan eksekusi pada titik await, lalu executor melanjutkan pekerjaan lain, sehingga banyak pekerjaan dapat dijalankan secara berselang-seling dalam satu runtime
  • Kebutuhan seperti timeout 3 detik dapat ditambahkan di async dengan kombinasi race dan Timer, tetapi pada kode thread sinkron diperlukan wrapper khusus TcpStream serta pengaturan timeout baca/tulis, sehingga generalisasinya lebih rendah
  • Jika async hanya dijelaskan lewat overhead performa, akan muncul kontra-contoh pada pekerjaan CPU bound; kekuatan Rust async terletak pada daya ekspresi semantik dan komposabilitas ekosistemnya

Titik Awal Masalah Konkurensi di Rust

  • Kode Rust biasa pada dasarnya memiliki struktur eksekusi linear
    • Seperti foo(), bar(), baz(), satu pekerjaan selesai sebelum pekerjaan berikutnya dijalankan
  • Untuk kasus yang harus menangani banyak pekerjaan secara bersamaan, seperti web server, struktur linear cepat mencapai batas
    • Dalam struktur yang menerima klien dengan TcpListener::accept() lalu menjalankan handle_client(), klien kedua harus menunggu saat klien pertama sedang diproses
    • Jika handle_client() memakan waktu beberapa milidetik dan ada 2 klien serentak, akan muncul penundaan singkat
    • Jika ada 2 juta klien serentak, pengguna di ujung antrean bisa menunggu beberapa menit

Cara Thread Menyelesaikannya

  • Thread sistem operasi dapat menyimpan nilai register dan stack program ke memori, menjalankan rutinitas lain, lalu melanjutkan kembali rutinitas semula nanti
  • Kode web server menyerahkan penanganan klien ke thread terpisah dalam bentuk thread::spawn(move || handle_client(client))
    • Thread utama terus melakukan accept() untuk koneksi baru
    • Jika thread penanganan klien terblokir, OS dapat kembali ke thread utama untuk menerima koneksi berikutnya
    • Dua klien dapat berjalan secara paralel setelah penundaan di kisaran beberapa mikrodetik
  • Jika web server kelas produksi memiliki puluhan core CPU, OS tidak hanya membuat thread tampak berjalan bersamaan, tetapi juga dapat benar-benar menjalankan beberapa thread sekaligus

Cara Kerja async/await

  • Konkurensi di ruang pengguna memiliki berbagai model, seperti pemrograman berbasis event, actor, dan coroutine; pendekatan yang dipilih Rust adalah async/await
  • Jika disederhanakan, program dikompilasi menjadi sekumpulan state machine yang dapat dijalankan secara independen
    • async fn bukan fungsi tradisional, melainkan fungsi yang mengembalikan state machine
    • await memasukkan state machine lain sebagai salah satu tahap dari state machine saat ini
    • Jika fungsi internal menyerahkan eksekusi, misalnya saat menunggu koneksi baru, seluruh state machine menyerahkan kendali ke executor di atasnya
  • Executor seperti smol::Executor menjalankan state machine lain yang dibuat dengan spawn, bukan state machine saat ini
    • Blok async move { handle_client(client).await } adalah state machine baru yang independen dari main
    • Saat main menyerahkan eksekusi, salah satu pekerjaan klien dijalankan; saat pekerjaan itu kembali menyerahkan eksekusi, giliran berputar ke pekerjaan berikutnya
  • Struktur ini memungkinkan penanganan jutaan klien secara bersamaan, tetapi juga menambah kompleksitas melalui konsep seperti executor, task, dan state machine

Komposabilitas yang Terlihat dari Contoh Timeout

  • Salah satu kekuatan Rust adalah komposabilitas
    • Iterator dapat diberi berbagai combinator, lalu hasilnya diteruskan lagi ke fungsi yang menerima Iterator
    • Seperti recv.try_iter().filter(...).map(...) pada mpsc::channel(), nilai dapat difilter dan ditransformasi lalu ditambahkan ke list
  • async/await memungkinkan komposabilitas seperti ini diterapkan juga pada fungsi I/O bound
  • Jika handle_client() adalah fungsi asinkron yang melakukan await pada read_to_end, do_something_with_data, dan write_all, timeout 3 detik dapat diimplementasikan dengan mengomposisikan dua Future
    • race menjalankan dua Future secara bersamaan
    • Timer kembali setelah waktu yang ditentukan berlalu
    • Cukup bungkus kode penanganan koneksi yang ada dalam blok async, lalu adukan dengan Future yang mengembalikan error TimedOut setelah 3 detik
  • Pendekatan ini tidak terikat hanya pada TcpStream
    • Pola yang sama dapat diterapkan pada target apa pun yang mengimplementasikan impl AsyncRead + AsyncWrite
    • Target seperti stream GZIP di atas stream biasa, socket Unix, atau file juga bisa digantikan

Batasan Saat Mengimplementasikan Timeout yang Sama pada Kode Thread Sinkron

  • Pada kode blocking, umumnya sulit menghentikan system call read atau write, dan cara seperti menutup file descriptor tidak dapat digunakan di Rust
  • TcpStream menyediakan set_read_timeout dan set_write_timeout
    • Timeout dapat diatur masing-masing untuk baca dan tulis
    • Namun jika klien mengirim 1 byte setiap 2,9 detik, timeout sederhana dapat terus direset
  • Untuk bertahan dari hal ini, perlu membuat tipe seperti DeadlineStream yang membungkus TcpStream, lalu setiap kali menghitung sisa waktu hingga deadline total dan mengaturnya sebagai timeout baca/tulis
  • Pendekatan ini bisa bekerja, tetapi sangat terbatas
    • Terikat pada TcpStream
    • Rust tidak memiliki trait yang mengabstraksikan penggunaan set_read_timeout dan set_write_timeout
    • Untuk menerapkannya pada writer generik, diperlukan banyak pekerjaan tambahan
    • Ada system call tambahan untuk mengatur timeout
    • Dalam logika web server nyata, penggunaannya bisa lebih merepotkan

Contoh dari Ekosistem async Rust

  • Salah satu alasan ekosistem HTTP, termasuk kliennya, mengadopsi async/await sebagai mekanisme runtime utama adalah komposabilitas fungsi
    • Fungsi yang membuat panggilan HTTP dapat dipasang ke berbagai celah dan kasus penggunaan
  • tower adalah contoh representatif yang menunjukkan komposabilitas async/await
    • Jika layanan diimplementasikan sebagai fungsi async, timeout, rate limiting, load balancing, hedging, dan penanganan backpressure dapat ditambahkan
    • Terlepas dari runtime apa yang dipakai atau apa yang dilakukan di dalam layanan, tower dapat diterapkan untuk meningkatkan ketangguhan
  • macroquad adalah game engine kecil untuk Rust, dan menjalankan engine dengan menggunakan async/await pada fungsi utama
    • async/await cocok untuk mengekspresikan situasi di Rust ketika fungsi linear harus berhenti untuk menunggu suatu pekerjaan
    • Konfigurasi seperti melakukan polling koneksi jaringan game server dan framework GUI secara bersamaan pada thread yang sama menjadi mungkin

Batasan Menjelaskan async Hanya dari Performa

  • Rust Async Book membandingkan bahwa thread OS mudah digunakan untuk mengekspresikan konkurensi tanpa mengubah model pemrograman, tetapi sinkronisasi antar-thread sulit, overhead performanya besar, dan bahkan thread pool pun sulit mendukung workload I/O bound berskala besar secara memadai
  • Di komunitas async, jawaban atas pertanyaan mengapa memakai async alih-alih thread OS cenderung berbunyi seperti “overhead-nya lebih rendah dan sisanya sama”
  • Alasan penulis web server beralih ke async/await adalah untuk menyelesaikan C10k problem, tetapi tidak semua pengguna harus memilih async/await karena alasan performa
  • Keuntungan performa bisa hilang tergantung situasi
    • Pada pekerjaan CPU bound, workflow berbasis thread bisa lebih cepat daripada workflow async yang setara
    • Keunggulan performa sementara dari Rust async selama ini terlalu ditekankan, sementara keuntungan semantiknya kurang dihargai
  • async/await bukan alat untuk kasus niche, melainkan model pemrograman kuat untuk menangani pola yang sulit diekspresikan secara ringkas di Rust sinkron tanpa puluhan thread dan channel

Menerima Perbedaannya, Bukan Membuatnya Seperti Rust sync

  • Roadmap proyek Rust memiliki arah bahwa menulis async Rust sesekali harus semudah menulis kode sinkron, selain penggunaan keyword async dan await
  • Namun ada juga pandangan bahwa framing untuk membuat async Rust “sama persis dengan sync Rust” pada dasarnya sulit
    • Meski bisa dibuat mirip hingga 99%, pengguna rata-rata tetap akan menyadari perbedaannya
  • Ekosistem async/await Rust sebaiknya tidak berusaha menjadi sama dengan Rust sinkron, melainkan lebih jelas menonjolkan kekuatannya dalam komposabilitas dan daya ekspresi
  • Agar async/await menjadi pilihan default saat konkurensi dibutuhkan, model ini perlu dijelaskan lewat alasan semantik, bukan alasan performa teknis semata

1 komentar

 
GN⁺ 2024-03-26
Opini Hacker News
  • async/await single-thread adalah model yang sederhana dan sudah dikenal luas; JavaScript memang seperti itu
    Thread memungkinkan beberapa CPU dikerahkan untuk menyelesaikan masalah, dan Rust membantu mengelola lock. Thread dengan prioritas berbeda juga bisa dibuat, sehingga mungkin diperlukan untuk pekerjaan yang compute-bound
    Sebaliknya, async/await multi-thread menjadi berantakan. Jika bagian compute-bound masuk secara serius, ia pada dasarnya memblokir thread yang dibagi dengan pekerjaan lain, sehingga modelnya mudah runtuh
    Multi-thread compute-bound di Rust tidak bekerja sebaik yang diharapkan. Di allocator penyimpanan, beberapa thread bisa menghantam lock yang sama dan menyebabkan keruntuhan akibat kemacetan futex; terutama jika saat memperbesar buffer terjadi penyalinan ulang sambil mengunci seluruh allocator, biayanya menjadi sangat mahal. Allocator library di dalam .DLL emulasi library Microsoft pada Wine rentan terhadap masalah ini, sehingga waktu CPU habis di spinlock dan performa turun puluhan kali lipat, sementara implementasi Microsoft tidak begitu
    Selain itu, Mutex standar dan channel crossbeam-channel bisa mengalami kelaparan akibat mutex yang tidak adil. Jika beberapa thread berulang kali mengunci resource, bekerja, lalu melepasnya, satu thread bisa terus menang sementara yang lain tersisih. Jika membutuhkan mutex yang adil, ada parking-lot, tetapi ia tidak memiliki keamanan poisoning saat thread panic seperti yang diberikan mutex standar
    Jika bukan I/O-bound, semuanya menjadi jauh lebih kompleks
    https://users.rust-lang.org/t/mutex-starvation/89080

    • Benar. Saya terutama hanya menangani komputasi yang I/O-bound, tetapi di sana pun muncul masalah kontensi
      Kalau throughput I/O kembali menjadi batasnya, apa artinya sejuta coroutine? Jika pool koneksi DB berukuran 10 langsung habis, coroutine tidak akan menyelamatkan Anda; ia hanya membuat debugging dan workaround lebih sulit serta lebih susah dinalar
    • Saya merasa masalah ini pada akhirnya mungkin perlu dipikirkan ulang di level hardware
      Masalah CPU-bound tampaknya bermuara pada interrupt/resume yang sistematis, dan jika untuk n thread eksekusi yang sedang berjalan dimungkinkan context switching berbasis antrean yang adil dan efisien, misalnya CPU dengan n context yang sedang berjalan, saya bertanya-tanya apakah masalahnya bisa menjadi masalah alokasi resource
    • Saya tidak mengerti mengapa sulitnya multitasking kooperatif terus ditemukan kembali
      Bahkan Go, yang saya anggap sebagai bahasa yang dirancang dengan bertanggung jawab, awalnya memilih pendekatan kooperatif tetapi akhirnya terpaksa beralih ke preemptive. Ini bukan berarti multitasking kooperatif tidak berguna, tetapi seharusnya diberi label peringatan; lebih jauh lagi, mungkin lebih baik jika jenis kode tertentu dicegah secara statis agar tidak dijalankan
      Sebagai tulisan terkait, saya sertakan “What color is your function”
      https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
    • Saya terus mengulang bahwa ini adalah detail implementasi
      Executor multi-thread untuk async/await bisa menangani starvation dengan cukup baik, dan implementasi .NET bahkan tahan terhadap kode yang sangat buruk yang mencampur panggilan blocking dan async
      https://news.ycombinator.com/item?id=39530435
      https://news.ycombinator.com/item?id=39786142
      https://news.ycombinator.com/item?id=39721626
    • Fungsi berwarna yang dihasilkan async/await juga menambah biaya pengembangan dan pemeliharaan software
      https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
      Jika bukan software yang membutuhkan skalabilitas tinggi, trade-off async mungkin tidak sepadan
  • Inti perdebatan async/await vs thread bukan soal mana yang lebih rumit, melainkan bahwa ia membelah ekosistem menjadi dua dan salah satu pihak menjadi warga kelas dua, sehingga jika salah memilih untuk proyek, akan timbul gesekan
    Keduanya bisa dicampur, tetapi ketika diperlukan caranya terasa hacky dan tidak efisien. Saat ini ekosistem Rust pada dasarnya sudah diputuskan agar begitu ada I/O, semuanya terikat ke ekosistem async/await, dan hampir semua hal yang ingin dilakukan di Rust—dengan sedikit pengecualian—melibatkan I/O, sehingga library non-async umumnya harus diabaikan, entah bagian aplikasi lainnya menginginkan async atau tidak
    Kalau Rust memakai abstraksi yang lebih mudah dikomposisikan daripada async/await, dan sifat komposisional itu tidak menuntut hal-hal lain ikut dibuat async/await, rasanya sebagian besar keluhan akan hilang

    • Saya setuju dengan diagnosisnya. Di tulisan saya tentang Rust async[0], saya juga sampai pada kesimpulan yang sama
      Yang lebih buruk, ekosistem bukan hanya terbelah dua; bahkan di dalam kode async pun biasanya ia sangat terikat pada executor, umumnya Tokio. Jika masalah warna fungsi diperluas, alih-alih biru (non-I/O), hijau (blocking I/O), merah (async I/O), kenyataannya menjadi biru, hijau, merah (Tokio), ungu (async-std), oranye (smol)
      Menurut saya, solusi terbaik untuk masalah ini adalah pola sans-I/O. Dengan memisahkan semua kode biru dan memakai inversi kendali untuk I/O dan waktu, logika protokol inti bisa dibuat tidak mengetahui I/O, dan mudah dibungkus dengan berbagai bentuk I/O
      0: https://hugotunius.se/2024/03/08/on-async-rust.html
    • Library non-async hanya perlu diabaikan jika ada dua library yang bisa dipilih dan selebihnya sama. Kasus seperti itu jarang
      Memakai kode blocking di aplikasi async memang tidak semulus yang diharapkan, tetapi juga tidak sulit. Alih-alih foo(), gunakan tokio::spawn_blocking(foo).await; itu akan menjalankan kode baru di thread terpisah dan mengembalikan future yang selesai ketika thread tersebut selesai
    • C# pada dasarnya juga mirip
      Untuk I/O memang ada opsi non-async, tetapi jika memakai opsi async, secara efektif Anda dipaksa membuat semuanya async sampai Main(). Ada juga cara aman memanggil metode async dari metode sinkron, tetapi itu membuat debugging menjadi luar biasa sulit
  • Ada banyak bagian yang terlewat dari tulisan itu
    Karena async/await berjalan dalam konteks satu thread, lock atau sinkronisasi tidak diperlukan, tetapi jika async/await dijalankan di banyak thread untuk memanfaatkan core CPU, lock dan sinkronisasi kembali diperlukan. Kompleksitas ini bisa disembunyikan dari kode eksternal. Misalnya, daripada menyinkronkan akses ke satu koneksi DB, lebih mudah membuka satu koneksi DB untuk tiap task async, tetapi pada SQLite atau PostgreSQL hal itu bisa memengaruhi performa
    Dalam async/await, propagasi error tidak jelas. Terutama ketika mencoba menggabungkan task async; Happy Eyeballs adalah contoh tipikal
    Jika membahas I/O jaringan, backpressure juga harus dibahas. Implementasi async/await CPython terkenal kekurangan backpressure jaringan, dan itu menimbulkan masalah

    • async/await punya banyak masalah, tetapi keluhan terbesar saya adalah ini
      “Design Patterns” dari Gang of Four sebagian besar adalah buku resep untuk mengakali kekurangan C++, tetapi orang-orang menerapkan pola-pola itu juga pada bahasa yang tidak memiliki kekurangan tersebut
      Rust bukan JavaScript, dan mampu menjalankan banyak thread dengan baik. Jadi sebenarnya tidak harus memakai async/await, dan sebagai bahasa sistem, Rust bisa mencoba solusi lain
      Namun untuk mendorong Rust ke programmer JavaScript, async/await dibutuhkan. without.boats menulis bahwa ia “mendorong async/await dengan semangat tulus berdasarkan asumsi bahwa kelangsungan hidup Rust bergantung pada fitur ini”
      https://without.boats/blog/why-async-rust/
      Apakah async/await cocok secara teknis untuk Rust tidaklah penting; kesannya karena programmer JavaScript sudah akrab dengan async/await, maka Rust juga harus memiliki async/await
    • async/await, seperti thread, juga merupakan mekanisme konkurensi, dan ketika mengakses memori bersama, lock selalu diperlukan. Saya tidak tahu dari mana munculnya pernyataan bahwa lock tidak diperlukan
    • async bisa lebih menakutkan dalam hal lock
      Sebuah blok kode mungkin bergantung pada akses eksklusif, dan karena tidak ada await, hal itu terjamin; tetapi jika await ditambahkan di tengah, kodenya rusak. Threading setidaknya memaksa kita menyatakan dalam kode apa saja yang membutuhkan akses eksklusif
      async juga berarti Anda sendiri mengelola penjadwalan thread. Jika I/O banyak dan kode CPU-bound pendek, itu baik-baik saja, tetapi jika ada kode CPU-bound, atau bahkan hanya sesekali ada, Anda akan bermain-main menjadi scheduler
    • Saat bergabung dengan tim yang memakai Node.js, saya mengalami masalah backpressure
      Beberapa service mati begitu saja dengan ABEND, dan dari sudut pandang orang yang datang dari Java, kelalaian seperti ini mengejutkan. Menjelaskan cara memperbaikinya kepada tim juga sulit
      Karena propagasi error, saya tidak akan memakai async/await jika punya pilihan. Untuk proyek sendiri mungkin saja, tetapi jika bekerja dengan orang lain, memakai library, dan harus menyamakan pemahaman semua orang, saya benar-benar tidak mau
      Saya belum benar-benar memakai konkurensi terstruktur di level bahasa, tetapi saya menaruh harapan pada Project Loom di Java. Dari kelihatannya, itu akan membuat perdebatan ini tidak relevan
    • Di Rust, async/await tidak hanya berjalan dalam konteks satu thread
  • Ada masalah dalam tulisannya
    Contohnya hanya satu server web, dan solusi di sisi thread juga diuraikan dengan keliru. Selain itu, pertanyaannya seolah-olah mengasumsikan bahwa orang-orang menginginkan thread OS alih-alih async/await
    Yang diinginkan programmer adalah thread secara konseptual dan semantik. Artinya memakai logika sekuensial dan tidak memakai anotasi aneh seperti async. Kalau async/await memang sebagus itu, bukankah semua fungsi bisa saja dibuat async secara implisit dan pemanggilan fungsi biasa dipakai alih-alih await? Kalau begitu, pada dasarnya kita memprogram dengan thread
    Thread OS mahal karena stack yang dialokasikan secara statis, dan yang kita inginkan adalah thread murah yang bisa menjalankan jutaan di satu CPU. Hanya saja tanpa kata-kata async/await yang kikuk itu. wait boleh tetap ada untuk penantian blocking dalam arti klasik, seperti menunggu event atau selesainya thread lain, tetapi saya tidak menginginkannya pada pemanggilan fungsi
    Kembali ke contoh server web, ketika timeout diimplementasikan dengan driver.race(timeout).await, apa yang terjadi pada socket klien setelah race melaporkan error timeout? Apakah ia tidak bocor dalam keadaan tetap terbuka dan terhubung?
    Versi timeout dengan thread juga bisa dibuat hampir mirip dengan async/await, seperti threaded_race(client_thread, timeout).wait. threaded_race melacak timeout secara paralel dengan thread memakai timer, lalu ketika waktunya tiba memanggil client_thread.interrupt() ala Java. Thread.interrupt() hanya memasang flag jika thread tidak sedang terblokir, dan melempar InterruptedException jika sedang terblokir pada panggilan I/O. Karena itu checked exception, compiler memaksa client.read_to_end(&mut data) dibungkus try/catch atau exception dideklarasikan pada handle_client, sehingga programmer tidak lupa menutup socket klien

    • Nilai-nilai internal race() akan di-Drop, dan driver sendiri tetap ada
      Jika tipenya dipakai apa adanya, Rust akan mengeluh karena Result tidak ditangani, dan jika socket baru dibuat secara lokal di dalam future, ia akan dibersihkan
      Hal bagus dari Rust future adalah kita bisa mendefinisikan semua perilaku di sekitarnya. Berbeda dari model di mana semua fungsi bersifat blocking, Rust bisa menentukan kapan eksekusi ditunda ke pekerjaan berikutnya di antrean kerja, dan melakukan polling pekerjaan secepat mungkin dengan state yang disimpan secara eksplisit (struct Future). Karena itu tidak perlu sleep() untuk yield seperti thread, sehingga cepat dan mudah dinalar
      Thread.interrupt di Java pun pada akhirnya mendekati loop sleep, dan mungkin baik-baik saja untuk sebagian besar aplikasi. Namun Rust adalah bahasa sistem, jadi pada sistem embedded pendekatan semacam itu tidak bisa dipakai, dan juga tidak diinginkan untuk kernel atau aplikasi latensi rendah
    • Sebagian programmer menginginkan logika sekuensial, tetapi banyak orang menginginkan hal yang justru sebaliknya
      Dalam kebanyakan kasus, orang tidak terlalu peduli apakah itu system call OS yang blocking atau non-blocking, tetapi mereka ingin memahami alur kontrol program yang sedang dibaca, tahu di mana ia menunggu, dan bagaimana ia bisa dijalankan secara paralel
      Justru ketika bekerja dengan fungsi blocking, saya berharap ada pasangan keyword blocking/block. Panggilan blocking bisa diam-diam membuat keseluruhan sistem melambat, dan saya sudah terlalu sering melihat aplikasi yang terasa lambat menjengkelkan karena system call blocking masuk ke UI thread
    • Cara membuat semua fungsi menjadi async implisit dan memakainya seperti pemanggilan fungsi biasa sudah dicoba berkali-kali selama beberapa dekade terakhir. Cari saja “RPC”
      Semua upaya menyatukan sinkron dan asinkron telah gagal. Ada perbedaan semantik besar antara kode yang berjalan di dalam satu thread, kode yang berjalan antar-thread, bahkan kode yang berjalan antar-komputer. Kalau mencoba mengabstraksikannya, pada akhirnya abstraksi itu tidak memadai, jadi lebih baik mempelajarinya dengan benar sejak awal
    • Saya ingat withoutboats pernah mengatakan di suatu tulisan bahwa jawaban sebenarnya adalah kompatibilitas dengan C
    • Ada juga cara menulis kode dengan poll() dan select(), tetapi itu pendekatan lain lagi
  • Menarik melihat kampanye yang nyaris seperti pemasaran untuk menyelamatkan muka async/await
    Berdasarkan pengalaman saya, ini bukan hanya kesalahan teknis, tetapi juga menimbulkan biaya besar bagi komunitas. Alih-alih berfokus pada fitur bahasa yang benar-benar berguna, upaya Rust terseret ke kekacauan ini
    Meski begitu, saya masih menaruh harapan besar pada bahasanya, dan menurut saya ini yang terbaik dari pilihan yang ada saat ini. Hanya saja saya khawatir pertarungan ini akan berlangsung selamanya
    P.S.: Contoh AsyncWrite/AsyncRead terlihat masuk akal, tetapi sebenarnya jika dibatasi ke *nix, hal yang sama juga bisa dilakukan dengan thread dan file descriptor

    • Saya pernah memakai async di firmware, dan itu benar-benar penyelamat
      Generalisasi seperti itu kurang berdasar dan tampaknya terlalu condong ke beban kerja tertentu
    • Saya tidak terlalu paham Rust, jadi tidak tahu apakah ini benar, tetapi secara empiris, 9 dari 10 diskusi Rust yang saya lihat di HN/reddit belakangan ini rasanya berkisar soal async
      Dari sudut pandang orang yang sama sekali tidak tertarik pada async dan hanya ingin membaca tentang Rust, ini cukup tidak menyenangkan
    • Kalau Anda berpikir thread lebih cepat daripada poll(), saya ingin tahu use case-nya apa. Seumur hidup saya belum pernah melihat kasus seperti itu
    • Ini bukan kesalahan teknis; ketika membutuhkan kode async dengan latensi sangat rendah, ini adalah solusi yang sangat bagus
      Kesalahannya adalah mendorongnya juga ke mayoritas use case yang tidak membutuhkan hal semacam itu
    • Saya penasaran apakah ada dasar untuk klaim bahwa pekerjaan async merampas fitur berguna lainnya
      Banyak proyek Rust utama bergantung pada async bukan hanya karena peningkatan performa besar dibanding alternatif berbasis thread, tetapi juga karena karakteristik desainnya. Pada beban kerja utama yang I/O-bound, manfaat seperti ini mudah terlihat. Fakta bahwa orang-orang pintar yang memecahkan masalah nyata mengadopsi async secara luas di crate-crate utama menurut saya adalah sinyal kuat bahwa async memang fitur bahasa yang berguna
      Pertarungannya terutama terjadi di Hacker News dan reddit, dalam bentuk orang-orang yang tidak membutuhkan async menjadi marah karena crate I/O yang mereka pakai kini menginginkan async. Saya mengerti ini situasi yang tidak menyenangkan, dan memang benar async punya masalah nyata yang masih sedang diselesaikan. Ini belum sempurna. Namun perpecahan soal async yang terlihat di forum rasanya tidak seluas atau sedramatis di proyek nyata
  • Yang banyak terlewat adalah pembatalan
    Future sangat mudah dibatalkan. Sebaliknya, membatalkan thread itu berantakan seperti main pukul tikus tanah, dan penghentian thread secara paksa tidak dapat dipercaya karena ada risiko lock tetap terkunci
    Dalam model async Rust, semua future bisa diberi timeout dari luar. Tidak perlu setiap fungsi I/O di ujung bawah mendukung opsi timeout, dan tidak perlu meneruskan timeout itu ke seluruh call stack
    Jika digabungkan dengan guard Drop, praktik terbaik Rust untuk mengelola status yang sedang berlangsung, bahkan pekerjaan besar dan kompleks pun dapat dibatalkan dengan mudah dan andal

    • Bukan semua future mudah dibatalkan, melainkan mudah berpura-pura bahwa semua future sudah dibatalkan
      Misalnya, meski sesuatu yang memakai spawn_blocking dibatalkan (drop), ia tetap berjalan di background, dan pengguna mungkin tidak mengetahuinya. Operasi filesystem async yang diimplementasikan dengan thread pool juga tetap berjalan meski dibatalkan
      Hal seperti ini bisa berujung pada bug yang sulit dipahami, seperti “saya yakin tidak ada apa pun yang menulis file itu, jadi kenapa service gagal karena file sedang digunakan?”
    • Jika async future bisa diimplementasikan, seharusnya thread yang dapat dibatalkan juga bisa diimplementasikan sebagai gantinya
      Masalahnya cukup isomorfik. System call memang sulit, tetapi baik dilakukan di thread maupun di async future, jika melakukan system call yang sama persis, masalah pembatalan yang muncul juga persis sama
    • Saya tidak mengerti kenapa pembatalan thread sulit
      Cukup sediakan status seperti flag yang bisa diakses semua thread, lalu buat loop pekerjaan memeriksa flag itu. Jika false, return lalu join thread, selesai
    • Berdasarkan pengalaman saya, pembatalan tidak terlalu layak dikhawatirkan
      Jika suatu pekerjaan tidak lagi berguna, cukup bila informasi itu pada akhirnya terlihat oleh fungsi yang dipanggil untuk melakukan pekerjaan tersebut. Kecuali tepat sebelum melakukan hal yang sangat mahal seperti memulai RPC, saya tidak repot-repot memeriksanya
  • Pertanyaan yang lebih baik menurut saya adalah “mengapa async/await, bukan fiber?”
    Saya tahu Rust pernah memiliki green thread sebelum 1.0 dan sengaja menghapusnya, tetapi ada berbagai pendekatan untuk implementasi concurrency berbasis fiber, misalnya cara yang tidak perlu menanamkan runtime yang berat ke dalam bahasa
    Kalau saya memahami tulisannya dengan benar, sepertinya yang terutama dipuji adalah bahwa future bisa di-drop kapan saja. Pada thread, hal serupa tidak bisa dilakukan karena alasan yang jelas, dan sekalipun secara teknis mungkin, itu akan sangat tidak aman. Namun kemampuan ini punya biaya yang sangat besar. Bukan hanya tidak bisa memakai executor berbasis completion seperti io-uring bersama array berbasis stack, atau menjalankan subtask di thread executor lain, tetapi juga muncul jebakan halus dan masalah reliabilitas yang menjadi kejutan sangat tidak menyenangkan saat memakai Rust sinkron
    https://smallcultfollowing.com/babysteps/blog/2022/06/13/asy...
    Menurut saya pembatalan task pada dasarnya harus berupa pembatalan kooperatif, dan pembatalan non-kooperatif itu secara permukaan memang praktis, tetapi lebih mirip fitur keliru yang menyimpan masalah dalam di bawahnya
    Selain itu, memuji composability async/await juga terasa aneh. Di Rust saat ini, yang tidak memiliki proper effect system, karena sifatnya yang menular, ia jauh dari mudah dikomposisikan. Misalnya, coba pakai method map di standard library bersama closure async, atau pakai trait standar io::Read/Write

    • Untuk memasukkan fiber ke Rust sebagai abstraksi concurrency kooperatif di user space, kita harus memaksakan banyak keputusan desain
      Perlu memilih apakah stack akan diimplementasikan sebagai spaghetti stack, mensyaratkan library memory mapping tingkat proses, atau membatasinya ke stack berukuran tetap
      Ketiga cara itu semuanya bermasalah saat berinteraksi dengan kode bahasa yang memiliki ABI berbeda. Misalnya, jika dari satu fiber kita memanggil kode C lalu kode C itu mencoba melanjutkan fiber lain, situasinya bisa menjadi cukup rumit
      Salah satu keunggulan async/await adalah keyword await itu sendiri. Berkat titik tunggu yang eksplisit, kita benar-benar bisa menalar interaksi dalam program concurrent
      Fiber yang melakukan yield mirip seperti goto di dunia concurrency. Saat memanggil suatu method, kita tidak tahu apakah sebagai efek samping eksekusi akan berhenti, atau saat dilanjutkan nanti keadaan dunia sudah berubah. Karena harus menulis kode secara defensif saat bersentuhan dengan dunia luar, fiber lebih cocok untuk task yang berjalan terisolasi dan berkomunikasi lewat completion
      Green thread, fiber, dan coroutine berbagi masalah yang sama di sini. Concurrency kooperatif di user space lebih mirip memindahkan berkas di atas meja daripada memecahkan bagian sulit dari concurrency. Rust async/await lebih eksplisit, sehingga tidak menyembunyikan efek samping yang disembunyikan mekanisme lain
    • Saya tidak tahu bagaimana fiber menyelesaikan masalah pembatalan. Bukankah hampir setara?
      Kode berbasis fiber terasa sulit diikuti karena harus melacak thread yang sedang berjalan di kepala. Setidaknya bagi saya, jauh lebih mudah melacak nilai yang nantinya selesai
    • Fiber dengan stack kurang cocok untuk kode low-level
      Lihat review Gor Nishanov untuk komite C++ http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p136.... Ini juga ditautkan di https://devblogs.microsoft.com/oldnewthing/20191011-00/?p=10.... Ringkasannya juga jelas: DO NOT USE FIBERS!
    • Karena ini Ruby, situasi thread/GVL berbeda dari Rust, tetapi mungkin yang dimaksud adalah hal semacam ini
      https://m.youtube.com/watch?v=qKQcUDEo-ZI
      Menurut saya ini cukup baik menunjukkan dasar argumen bahwa async/await itu menular dan canggung, sementara fiber—setidaknya dalam implementasi Ruby—adalah paradigma yang jauh lebih baik
    • Dalam persoalan matematika, pembatalan kooperatif bisa cukup menjengkelkan
      Algoritme optimisasi bisa memanggil persoalan pencarian akar, yang kemudian memanggil integrator ODE, dan seterusnya secara bertingkat; tahap mana pun bisa berjalan sangat lama. Token pembatalan harus diteruskan ke mana-mana, tetapi framework komputasi numerik biasanya tidak mendukungnya
      Kita bisa, dan memang seharusnya, memberi batas jumlah iterasi pada semua algoritme, tetapi pada algoritme bertingkat itu hanya menjamin berhenti dalam tahun ini, bukan menjamin berhenti dalam 5 detik
      Dalam masalah seperti ini, saya bisa menjamin bahwa yang saya lakukan hanyalah komputasi matematika massal, alokasi dan page fault yang menyertainya, tanpa I/O, serta menulis string log ke objek Queue standard library yang akan diproses di thread Main yang tidak akan dibatalkan. Fitur lain yang diperlukan juga bisa dikembalikan ke main thread lewat Queue
      Rasanya di abad ke-21 masalah ini seharusnya bisa diselesaikan tanpa harus memaksa token pembatalan diteruskan ke mana-mana, dan tanpa membuat orang menulis kode yang berjalan lama secara defensif kalau tidak ada pengecekan token
  • Dalam satu lagi diskusi async/await, orang-orang tidak memahami async/await, tidak bisa membayangkan mengapa mekanisme concurrency dibutuhkan di satu thread, dan berasumsi tidak ada yang membutuhkannya
    Pemrograman UI, komunikasi dengan GPU, dan komunikasi antarruntime adalah contoh yang bagus, dan pasti ada lagi
    Thread, baik green thread maupun bukan, tidak cocok untuk kasus seperti ini, tetapi async/await cocok

    • Di GUI, thread bisa digunakan dengan mudah, dan dulu saya pernah menulis beberapa aplikasi GUI yang memanfaatkan thread dengan cukup efektif
    • Kemampuan mengelola beberapa task secara eksplisit di satu thread jelas penting
      Jika memungkinkan implementasi fitur bahasa lain yang menghasilkan binary yang sama tetapi tidak terlalu merepotkan pengguna bahasa, itu juga layak dibahas
  • Salah satu keunggulan utama Rust async/await adalah ia bisa berjalan bahkan dalam situasi tanpa thread maupun memori dinamis.
    Ini sudah cukup berguna untuk menulis kode ringkas pada mikrokontroler yang menunggu hingga interrupt membaca data I2C yang masuk ke suatu buffer. Ini adalah abstraksi tingkat lebih tinggi yang memungkinkan penggunaan konkurensi tanpa terlalu banyak mengekspos interaksi dengan runtime di bawahnya.
    Semua perangkat lunak besar yang pernah saya kerjakan mengimplementasikan hal seperti ini dalam bentuk tertentu. Bahkan pada kode yang tidak memiliki konsep coroutine C++ modern, kami memakai Apple Grand Central Dispatch, Intel Threading Building Blocks, dan sebagainya. Jika tidak, business logic akan terblokir dengan sangat tidak efisien pada I/O, atau jumlah thread menjadi sangat banyak sehingga pengembangan dan debugging berubah jadi neraka, atau kode dipenuhi detail implementasi runtime di bawahnya, atau campuran dari ketiganya.
    Jika tidak memakai abstraksi yang sudah ada di bahasa itu sendiri atau library, pada akhirnya Anda akan membuatnya sendiri, dan itu sulit serta kemungkinan besar secara keseluruhan lebih inferior dibandingkan sesuatu yang digunakan luas. Dulu saya juga pernah membuatnya sendiri untuk C++: https://github.com/goto-opensource/asyncly

  • Sepertinya penulis mencampuradukkan dua hal.
    Yang satu adalah thread ruang pengguna/green thread, dan yang lain adalah konkurensi terstruktur.
    Yang pertama memang keunggulan async/await, tetapi bukan keunggulan yang unik. Ada contoh yang memungkinkan hal itu tanpa masalah warna fungsi, seperti Go atau Java Loom.
    Yang kedua bisa diimplementasikan baik dengan thread OS maupun green thread. Lihat JEP Structured Concurrency milik Java.
    https://openjdk.org/jeps/462