1 poin oleh GN⁺ 2023-10-08 | 1 komentar | Bagikan ke WhatsApp
  • Perdebatan runtime async Rust lebih dekat pada pilihan antara menyeimbangkan beban dengan work-stealing alih-alih “OS thread per core”, atau mengurangi perpindahan data dengan pendekatan share-nothing
  • Rust async sudah memenuhi syarat inti thread-per-core, yaitu konkurensi di ruang pengguna dan I/O asinkron, dan isu yang tersisa adalah seberapa jauh berbagi state antar-core bisa dihindari
  • Work-stealing mencoba mengurangi waktu idle thread untuk memperbaiki tail latency dan utilisasi CPU, tetapi di Rust state yang melintasi titik yield harus aman untuk thread sehingga muncul batasan Send
  • Share-nothing dapat menahan data di cache core tertentu untuk menurunkan latensi, tetapi tingkat kesulitannya sangat berbeda antara kasus seperti penyimpanan key-value yang mudah dipartisi dan kasus yang memerlukan transaksi serta perubahan atomik
  • Makalah Enberg membandingkan share-nothing dan shared-state, tetapi karena keduanya sama-sama tidak memakai work-stealing, hasilnya sulit langsung dihubungkan dengan kritik terhadap eksekutor work-stealing Rust

Inti perdebatan runtime async Rust

  • Di komunitas Rust, perdebatan terus berlanjut soal pilihan runtime async utama seperti tokio yang secara default memakai eksekutor multithread, lalu menyeimbangkan banyak task secara dinamis lewat work-stealing
  • Sebagian pengguna mengkritik bahwa default ini merusak pengalaman menulis kode karena memaksa batasan Send + 'static atau Send + Sync + 'static
  • Ada juga yang lebih menyukai server single-thread, tetapi di sini diasumsikan sistem Rust ingin menggunakan waktu dari satu atau lebih core CPU
  • Alternatif yang diajukan, yaitu “thread-per-core”, sering diharapkan lebih cepat dan lebih mudah diimplementasikan, tetapi sulit mendapatkan performa dan kemudahan implementasi sekaligus

Kebingungan yang ditimbulkan oleh nama “thread-per-core”

  • Eksekutor async multithread yang ada sekarang pun, dalam arti luas, juga termasuk thread-per-core
    • Membuat satu OS thread per core
    • Menjadwalkan jauh lebih banyak task daripada jumlah core di atas thread-thread tersebut
  • Pekka Enberg merangkum thread-per-core sebagai gabungan dari tiga ide
    • Menangani konkurensi di ruang pengguna alih-alih memakai kernel thread yang mahal
    • Menggunakan I/O asinkron agar thread per core tidak terblokir
    • Mempartisi data di antara core CPU untuk menghilangkan biaya sinkronisasi dan perpindahan data antar-cache CPU
  • Enberg menilai dua poin pertama penting untuk membangun sistem throughput tinggi, tetapi poin ketiga mungkin hanya diperlukan pada mesin multicore yang sangat besar
  • Jika memakai async Rust, dua syarat pertama—konkurensi ruang pengguna dan I/O asinkron—sebenarnya sudah terpenuhi
  • Karena itu, pusat perdebatannya bukan thread-per-core itu sendiri, melainkan optimasi mana yang dipilih antara work-stealing dan share-nothing

Tujuan dan biaya work-stealing

  • Work-stealing adalah optimasi untuk memastikan semua thread selalu punya pekerjaan sehingga tail latency berkurang
  • Dalam sistem nyata, jumlah kerja yang dibutuhkan tiap task berbeda-beda
    • Satu permintaan HTTP bisa memerlukan jauh lebih banyak kerja dibanding permintaan lain
    • Walaupun pekerjaan awal dibagi rata ke tiap thread, perbedaan task yang tak terduga dapat membuat beban berubah seiring waktu
  • Pada beban maksimum, sebagian thread bisa menerima lebih banyak kerja daripada yang mampu diproses, sementara thread lain tetap idle
  • tokio, async-std, dan smol semuanya mengimplementasikan work-stealing dengan tujuan mengurangi tail latency dan meningkatkan utilisasi CPU
  • Biayanya adalah task yang berhenti di satu thread bisa dilanjutkan lagi di thread lain
    • State yang dipakai melintasi titik yield harus aman untuk thread
    • Dalam API Rust, ini muncul sebagai keharusan bahwa future harus Send
    • Jika state sistem tidak dipahami dengan jelas, sulit menentukan bagaimana menjamin Send
  • Saat state berpindah ke thread lain, akan muncul biaya sinkronisasi dan cache miss, yang bertentangan dengan prinsip share-nothing di mana tiap CPU hanya menangani state miliknya sendiri

Logika performa share-nothing

  • Share-nothing adalah desain untuk menurunkan tail latency dengan menahan data di cache yang lebih cepat milik core CPU tertentu, bukan di cache lambat yang dibagi banyak core
  • Makalah Enberg The Impact of Thread-Per-Core Architecture on Application Tail Latency menunjukkan perbaikan tail latency dengan membenchmark penyimpanan key-value share-nothing dan struktur memcached berbasis shared-state
  • Cara makalah ini dikutip secara sederhana di komunitas Rust sebagai “peningkatan performa 71%” terasa dangkal dan tidak membantu
  • Penyimpanan key-value milik Enberg membagi state dan koneksi untuk membentuk struktur share-nothing
    • Mempartisi keyspace ke banyak thread dengan fungsi hash
    • Membagi koneksi TCP masuk antar-thread dengan SO_REUSEPORT
    • Merutekan permintaan dari thread pengelola koneksi ke thread pengelola keyspace terkait melalui channel pengiriman pesan
  • memcached membagi kepemilikan keyspace yang telah dipartisi ke semua thread, dan tiap partisi dilindungi dengan mutex
  • Hasil Enberg menunjukkan bahwa struktur berbasis channel dapat mencapai tail latency yang lebih rendah daripada mutex
    • Ini bisa dipahami sebagai berkurangnya cache miss karena partisi yang sama diakses berulang kali dan tetap berada di cache satu core
  • Namun, sulit mengatakan bahwa desain yang menghindari perpindahan data dengan fitur kernel tingkat lanjut dan struktur yang direncanakan hati-hati lebih mudah diimplementasikan daripada sekadar membungkus data di dalam mutex

Kasus ketika partisi state mudah dan ketika sulit

  • Penyimpanan key-value adalah contoh yang cocok untuk share-nothing karena state aplikasi mudah dibagi ke banyak thread
  • Pada aplikasi yang lebih kompleks, jika state dari beberapa partisi harus diubah secara transaksional atau atomik, implementasi yang benar membutuhkan perhatian jauh lebih besar
  • Dukungan terhadap share-nothing punya kemiripan dengan antusiasme berlebihan masa lalu terhadap basis data eventual consistency
    • Performanya memang bisa membaik
    • Tetapi dibutuhkan perancangan yang hati-hati agar terhindar dari bug akibat inkonsistensi data

Jarak antara makalah Enberg dan perdebatan work-stealing di Rust

  • Implementasi Enberg maupun memcached sama-sama tidak menggunakan work-stealing
  • Karena itu, hasil performa utama dari makalah tersebut sulit dihubungkan langsung dengan arsitektur work-stealing Rust
  • Jika work-stealing ditambahkan ke struktur Enberg, perpindahan data mungkin agak meningkat, tetapi itu juga bisa menjadi cara untuk meningkatkan utilisasi CPU
  • Untuk kasus menambahkan work-stealing ke memcached, sulit membayangkan alasan mengapa itu tidak membantu
  • Implementasi dalam makalah dirancang agar pembagian keyspace seimbang dan SO_REUSEPORT membagi pekerjaan secara merata sejak awal
  • Di lingkungan nyata, ketidakseimbangan dinamis bisa muncul
    • Hot key dapat menerima lebih banyak baca-tulis sehingga menambah beban thread yang mengelola keyspace tersebut
    • Sebagian koneksi dapat menangani jauh lebih banyak permintaan daripada koneksi lain sehingga menaikkan beban thread yang mengelola koneksi itu
  • Benchmark dalam makalah tampaknya tidak mereproduksi kondisi tidak seimbang ini karena setiap koneksi melakukan jumlah kerja yang tetap terhadap key acak
  • Bahkan di dalam sistem share-nothing, ketidakseimbangan bisa diredam dengan desain seperti menyimpan cache hot key di partisi tambahan
  • Sebagian task juga bisa dipaku ke core tertentu untuk menghindari perpindahan state, sambil tetap memakai bentuk work-stealing lain sebagai optimasi

Kesimpulan praktis

  • Jika sistem dirancang dengan hati-hati untuk menghindari perpindahan data antar-cache CPU, performanya bisa lebih baik daripada sistem yang tidak melakukannya
  • Tetapi jika keluhan terbesar hanya sebatas harus menambahkan batasan Send pada generics, sulit mengatakan bahwa itu sudah masuk kategori rekayasa performa sedetail itu
  • Jika sistem pada akhirnya tetap akan memakai shared state, sulit beranggapan bahwa work-stealing tidak akan memperbaiki utilisasi CPU saat berada di bawah beban

1 komentar

 
GN⁺ 2023-10-08
Pendapat di Hacker News
  • Secara pribadi, tulisan ini terasa seperti kehilangan gambaran besar dan terpaku pada detail
    Inti perdebatannya bukan executor work-stealing thread-per-core, melainkan apakah async/await di Rust merupakan abstraksi yang baik untuk itu
    Semakin banyak memakai kode async, semakin terasa abstraksinya bocor dan sulit diprogram terhadapnya
    Model konkurensi alternatif yang diinginkan orang adalah konkurensi terstruktur melalui coroutine ber-stack dan channel di atas executor work-stealing
    Sampai ada yang mengimplementasikannya dan membandingkannya dengan model berbasis async/await dan future, diskusi produktif akan sulit; orang yang tidak suka async akan menghindarinya, sementara orang yang tidak peduli pada Send + Sync + 'static akan terus memakainya

    • Kalau begitu, kecuali benar-benar membutuhkan skala C10M, rasanya cukup gunakan thread per koneksi dan jangan pakai yang lebih rumit
      Kebutuhan pada skala seperti itu tampaknya sangat jarang dalam kebanyakan kasus
      Tulisan semacam ini biasanya hanya berkata “kernel thread itu mahal” lalu melewatinya seolah-olah itu benar secara hakiki, padahal kenyataannya tidak begitu
      Jika pekerjaannya bukan terus-menerus membuat task yang tidak melakukan apa-apa, overhead “thread sungguhan” kemungkinan kecil, sementara kesederhanaan yang didapat sangat besar
    • Saya melihat perdebatan async/await vs coroutine ber-stack lebih menarik, tetapi perdebatan dalam tulisan ini bukan itu
      Kutipan yang dibahas withoutboats adalah bagian dari tulisan tertaut yang secara spesifik mengkritik multithreading default dan work-stealing
      [1] https://www.reddit.com/r/rust/comments/16p47f1/the_state_of_...
    • Ada banyak topik di dunia yang orang-orang tidak sepakati
      Tulisan ini hanya membahas perdebatan lain, bukan perdebatan yang saya harapkan ditulis
    • Saat memakai async/await bersama channel, kita bisa membatasi argumen fungsi async hanya menerima referensi immutable atau tipe copy, dan membuat tipe bersama yang mutable berkomunikasi lewat channel
      Kita juga bisa membuat server ala Erlang yang memiliki tipe mutable dan berkomunikasi lewat channel, atau tetap memaksakan dengan Arc
      Rust memberi kekuatan untuk melakukan keduanya
    • Sejauh yang saya pahami, tidak ada yang menghalangi penggunaan channel di atas dukungan async Rust yang sudah ada
      Coroutine ber-stack tampaknya tidak begitu berarti pada titik itu, karena kalau sudah begitu cukup pakai thread terpisah saja
  • Sekitar 15 tahun lalu, masalah awal yang ingin diselesaikan model thread-per-core adalah mendapatkan skalabilitas dan efisiensi komputasi pada server multicore serbaguna
    Berbeda dari beberapa klaim, thread-per-core secara eksplisit ditujukan untuk mengoptimalkan beban kerja berpusat CPU, dan meski kemudian membutuhkan penanganan I/O yang lebih canggih, ternyata model ini juga unggul untuk beban kerja berpusat I/O dengan throughput tinggi
    Saat membaca tulisan seperti ini, rasanya seperti melihat berbagai kesalahan desain perangkat lunak yang terjadi saat arsitektur thread-per-core diperkenalkan terulang dengan cepat
    Agar adil, ilmu komputer terkait thread-per-core terutama berasal dari HPC dan dokumentasinya kurang
    Tulisan ini berfokus pada masalah sulit dalam arsitektur thread-per-core, yaitu penyeimbangan kerja antar-core
    Model dasarnya adalah empat kombinasi push/pull data/beban, dan work-stealing pada dasarnya adalah model load pull
    Cara ini hanya ber-overhead rendah ketika hampir tidak perlu digunakan, yaitu ketika ada keseimbangan alami yang jarang muncul dalam masalah nyata
    Pada beban kerja yang lebih menarik, tempat ketimpangan beban dinamis antar-core umum terjadi, overhead koordinasi membuat work-stealing menjadi bottleneck performa
    Meski begitu, karena mudah dipahami, ia masih dipakai untuk beban kerja yang cocok, tetapi tidak mudah digeneralisasi
    Di antara beban kerja langka yang tidak disebut dalam tulisan itu, mungkin ada kasus ketika ini adalah pilihan terbaik
    Model yang tampaknya paling mendapat momentum belakangan ini adalah data push, yang kurang intuitif tetapi membutuhkan jauh lebih sedikit koordinasi antar-thread
    Model ini juga tidak cocok untuk sebagian beban kerja, tetapi dapat digeneralisasi dengan baik untuk sebagian besar beban kerja umum
    Arsitektur thread-per-core akan tetap ada. Untuk skalabilitas dan efisiensi, sulit dikalahkan
    Namun kebanyakan software engineer kurang memiliki intuisi tentang seperti apa desain thread-per-core modern dan idiomatis, dan ini diperburuk oleh sedikitnya tulisan atau makalah yang membahas topik ini secara mendalam

    • Alasan utama framework async Rust memakai work-stealing tampaknya karena mudah diaktifkan di tingkat framework dan meningkatkan performa banyak aplikasi, terutama aplikasi yang tidak dirancang secara ideal
      Melihat bio di profil dan penjelasan ini, sepertinya aplikasi semacam itu bukan jenis yang biasanya Anda tangani
      Kalau ada tautan ke literatur terkait, saya ingin membacanya
    • Saya penasaran apa maksud dari gagasan data push
  • Saya setuju dengan kalimat, “Maksudnya berorientasi I/O adalah bahwa ketika benar-benar ditulis dengan Rust, bebannya tidak cukup banyak untuk menjenuhkan satu core. Kalau begitu, tentu saja tulislah sistem single-thread”
    Banyak aplikasi yang saya pakai adalah daemon yang bereaksi terhadap event di latar belakang, dan jika dibuat single-thread, overhead Arc dan Mutex bisa dihilangkan
    Overhead ini pada titik itu kebanyakan lebih seperti beban sintaksis, tetapi debugging dan pemeliharaannya jadi lebih mudah
    Hal yang saya sukai dari Rust adalah hanya membayar biaya untuk hal yang memang diperlukan
    Tulisan asli yang ditanggapi artikel ini mengkritik tokio dan library async lain karena membuat sulit untuk kembali ke arsitektur single-thread yang sederhana
    Memang ada unsur berlebihan, tetapi secara umum saya setuju dengan kritik itu
    Membuat default menjadi lebih kompleks dengan alasan lebih baik untuk aplikasi throughput tinggi terlihat bertentangan dengan ideal Rust

    • Saya pernah menulis layanan seperti itu, tetapi saya mungkin tidak akan menyebutnya berorientasi I/O
      Bentuknya bukan terikat throughput, melainkan sebagian besar menganggur lalu, saat ada pekerjaan, menyelesaikannya cepat untuk mengurangi penggunaan resource sistem
      Kecuali sesekali ada lonjakan pekerjaan sangat besar dan latensi saat itu sangat penting, memakai lebih dari satu thread hanya menambah kompleksitas dan overhead tanpa manfaat
    • Benar. Tidak semua yang menginginkan concurrency adalah web server
      Di sistem operasi, semua layanan sistem harus menangani permintaan IPC secara bersamaan, tetapi sebagian besar melakukannya secara single-thread untuk mengurangi konsumsi CPU total
      Pada perangkat 4-core, membuat puluhan layanan dengan thread-per-core adalah pemborosan CPU dan RAM
    • tokio mendukung executor single-thread yang bisa dipakai saat benar-benar diperlukan, dan itu juga tidak sulit
      Di API tokio namanya LocalSet
      https://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html...
    • Saya juga hendak menulis tentang kutipan yang sama
      Meski satu thread pada satu CPU sudah cukup, kita tetap bisa menginginkan concurrency
    • Alih-alih Arc dan Mutex, kemungkinan akan memakai Rc dan RefCell, dan dari sisi kode rasanya sama-sama rumit dan verbose
      Saya paham itu kurang efisien, tetapi dalam kasus yang dijelaskan, biaya tambahan beberapa operasi atomik bukankah tetap bisa diabaikan?
  • Soal kutipan itu, saya setuju bahwa ungkapan “dosa asal pemrograman async Rust adalah menjadikan default-nya multi-thread… Send + 'static, atau lebih buruk Send + Sync + 'static, membunuh kesenangan memakai Rust” terdengar terlalu melodramatis
    Saya tidak melihat menghilangkan Send + Sync akan membuat perbedaan sebesar itu
    Yang paling mengganggu adalah 'static, dan itu bukan karena work stealing
    Yang diinginkan adalah concurrency yang memiliki scope
    Misalnya seperti <https://github.com/tokio-rs/tokio/issues/2596>
    Hal lain yang sekarang benar-benar saya benci dari Rust async adalah kurangnya instrumentation
    Di perusahaan, ada masalah produksi ketika beberapa task begitu saja berhenti, dan saya ingin melakukan sesuatu yang setara dengan gdb; thread apply all bt
    Setidaknya saya berharap <https://github.com/tokio-rs/tokio/issues/5638> masuk
    Saat ini pun sudah ada secara eksperimental, tetapi menurut pengalaman saya kadang menyebabkan panic
    Hari ini saya benar-benar menulis PR untuk mencoba memakai versi eksperimental itu saat SIGTERM
    Pertimbangannya, karena prosesnya memang sedang shutdown, crash pun tidak masalah
    Tidak satu pun dari keluhan ini akan terselesaikan dengan menghilangkan work stealing
    Kalaupun daftarnya diperpanjang, menghapus work stealing hampir tidak akan membantu apa pun

    • Java memang banyak dikritik, tetapi bagian ini sudah berjalan baik begitu saja sejak sekitar 25 tahun lalu dan sangat berguna untuk memecahkan masalah
      Tidak perlu debugger; kirim SIGQUIT ke JVM, maka stack trace semua thread akan di-dump ke stderr dan proses tetap berjalan
      Termasuk juga lock apa yang sedang dipegang atau ditunggu oleh tiap thread
      Setiap kali memakai bahasa lain, saya merindukan fitur ini
      Ini juga bisa dipakai untuk profiling ad hoc di produksi
      Ambil beberapa snapshot, lalu cari hotspot dengan grep/sed/sort/uniq
    • Untuk meringankan masalah seperti ini, di perusahaan kami memakai library yang secara berkala melakukan logging task mana yang sedang berjalan dan setiap task saat ini berada di file/baris nomor berapa
      Memang harus menaburkan r.set_location(file!(), line!()); secara manual sebelum setiap titik await, tetapi ini sudah beberapa kali membantu menjelaskan mengapa sistem terlihat seperti berhenti
      [1] https://github.com/antialize/tokio-tasks/blob/main/src/run_t... memiliki set_location(), dan task.rs memiliki list_tasks()
  • Tidak ada jawaban benar untuk masalah ini; semuanya bergantung pada use case
    Pada akhirnya ini soal beban kerja yang berpusat pada I/O versus CPU, dan seberapa buruk hal-hal seperti cache eviction atau lock contention berdampak
    Jika Anda menaruh server HTTP yang berkomunikasi dengan basis data eksternal dan memiliki logika bisnis ringan di tengahnya pada server virtual bersama, work stealing dan penggunaan ulang thread secara intuitif masuk akal
    Tentu saja, tetap harus selalu melakukan benchmark
    Sebaliknya, jika Anda sedang membuat basis data atau sistem serupa, dan konkurensi tinggi serta banyak context switch di bawah beban menyebabkan cache eviction dan contention di mana-mana, Anda akan kerepotan
    Dalam kasus ini, thread-per-core sangat masuk akal, dan framework async itu sendiri mungkin tidak ada gunanya
    Tidak ada jawaban dogmatis tentang mana yang “lebih baik”
    Anda harus melakukan profiling pada aplikasi
    Seperti yang pernah saya katakan, saya merasa fokus Rust secara keseluruhan sedang terdistorsi oleh arus masuk besar-besaran dari pengembangan layanan web
    Saya belum yakin apakah Rust adalah bahasa yang cocok untuk pekerjaan seperti itu, tetapi tampaknya cukup bekerja dengan baik bagi mereka, jadi ya sudahlah
    Namun diskusi publik tentang bahasa ini dan crate-crate yang terdorong ke depan saat ini secara umum mencerminkan bias ini
    Ini juga bias banyak software engineer di forum ini

    • Saya tidak banyak melihat masalah async memengaruhi bagian lain dari Rust
      Aplikasi sistem seperti game engine, pustaka kriptografi, kernel, alat command-line, compiler, dan sebagainya sedang berhasil dibuat tanpa menyentuh async
      Saya memelihara pustaka kriptografi besar, dan sama sekali tidak terpengaruh oleh urusan async
    • Saya setuju soal masuknya pengembang web
      Akan disayangkan jika akibatnya kegunaan Rust untuk pemrograman sistem menjadi rusak
    • Saya justru mengira web app ideal untuk thread-per-core
      Aplikasi itu sendiri hampir tidak punya state di luar request, dan socket listener serta koneksi basis data bisa dipisah per thread
      State yang tersisa pun kemungkinan besar sebagian besar statis antar-request, sehingga cache invalidation tidak sering terjadi
      Karena tidak ada shared state, penanganan ownership juga seharusnya mudah
    • Saya pikir ini terjadi karena komunitas Rust cukup aktif menarik pengembang semacam itu demi mendapatkan pasar dan awareness
      Saya tidak sedang mengatakan itu baik atau buruk, tetapi sekarang Rust harus hidup dengan aliran tanpa akhir pustaka dan framework terkait web yang kualitasnya beragam
      Dan karena cukup banyak pustaka dasar inti dan crate mengambil pendekatan async-first, async akan tetap menjadi topik diskusi
      Sekarang, kecuali pengembang biasa menetapkan sebagai kebijakan utama proyek bahwa mereka tidak akan memakai async, menulis kode sinkron biasa untuk masalah bisnis sudah menjadi sulit
  • Menurut saya, mewajibkan batasan Send agar task dapat dipindahkan antar-thread executor adalah cacat yang jelas dalam sistem async Rust itu sendiri
    Sebab, bersama dengan masalah mendasar async Drop, hal itu menghalangi implementasi API berskop
    Seperti halnya thread, seharusnya cukup jika batasan Send hanya ada pada fungsi seperti spawn atau pengiriman data melalui channel
    Pendekatan tanpa berbagi biasanya tidak lebih dari workaround untuk menyembunyikan cacat ini
    Mengikat task secara opsional ke thread/core tertentu memang punya kelebihan dan benar-benar berguna dalam situasi tertentu, tetapi itu diskusi yang lebih rinci dan tidak banyak berkaitan dengan keluhan pengguna async terkait Send

  • Tulisan yang bagus, dan saya menyarankan agar tidak berhenti pada judulnya saja, melainkan membaca lebih lanjut
    Kalimat yang paling saya sukai adalah “jika keluhan terbesar seseorang adalah menambahkan batasan Send pada suatu generic, sulit percaya bahwa orang itu sedang melakukan engineering pada level tersebut”
    Edit: Saya sepenuhnya setuju dengan komentar “duped”
    Karena saya tidak mengetahui konteks yang lebih besar dari diskusi ini, mungkin saya terlalu terburu-buru mengutip kalimat itu

  • Bagian “orang yang tidak bisa melihat state sistem dengan benar mungkin kesulitan menemukan cara terbaik untuk memastikan future menjadi Send” terdengar agak arogan, menurut saya
    Masalah lifetime 'static dan batasan Send/Sync sangat dirasakan oleh para pengembang, dan saya tidak mendapat kesan bahwa mereka bodoh

    • Itu merujuk kembali pada hal yang dikatakan sebelumnya
      Orang-orang mengatakan bahwa tidak melakukan work stealing lebih mudah dan lebih cepat
      Argumen saya adalah salah satu dari dua hal itu
      Agar pendekatan tanpa berbagi lebih cepat, Anda harus merancang kode dengan cara yang tidak lebih mudah daripada membuat arsitektur shared state menjadi thread-safe
      Di paragraf berikutnya ada kalimat paralel yang menjadi padanan untuk “lambat”
      Saya tidak menganggap orang-orang yang kesulitan membuat Rust paralel dan konkuren bisa dikompilasi itu bodoh
      Saya hanya tidak suka ketika mereka bertindak seolah-olah API yang kami buat telah menghancurkan hidup mereka
    • Kata “sulit” berada dalam tanda kutip karena itu belum tentu lebih sulit
      Jika itu memang sesuatu yang harus dilakukan, maka itu bukan lebih sulit
      Ini lebih ke arah bahwa async membuat masalah yang suatu saat tetap harus diselesaikan menjadi diselesaikan lebih awal, bukan “orang-orang bereaksi berlebihan terhadap pekerjaan sepele”
      Rasanya mirip dengan borrow checker
      Kadang memang terlalu membatasi, tetapi kadang ternyata memang ada edge case yang tidak dipertimbangkan ketika kita berasumsi semuanya baik-baik saja
    • Secara pribadi, saya pun kadang jelas kesulitan memahami apakah state saya Send atau tidak
      Jadi kalimat yang dikutip itu terasa mengena bagi saya
  • Ini adalah tulisan yang terlalu terpaku pada detail dan kehilangan gambaran besar
    Tidak ada cara yang selalu benar untuk mendapatkan performa terbaik di semua program
    Kita bisa berdebat panjang, tetapi kelebihan dan kekurangan thread-per-core adalah contoh klasik “tergantung situasinya”
    Masalahnya sejak awal adalah penggunaan async itu sendiri merupakan optimisasi prematur
    99% program Rust bukanlah sesuatu seperti redis atau linkerd
    Itu adalah alat CLI atau web app yang sudah cukup cepat meski ditulis dengan Python atau Ruby
    Jadi saya bertanya-tanya mengapa komunitas meninggalkan blocking I/O Rust sehingga semuanya menjadi async, dan para pengembang menempelkan #[tokio::main] pada segala hal seolah-olah itu default

    • Salah satu alasannya mungkin karena jika performa thread-per-core sudah cukup, besar kemungkinan Anda sejak awal tidak ingin memakai Rust
      Ada bahasa yang menawarkan pengalaman pemrograman lebih baik dengan imbalan kecepatan, dan Python adalah salah satu contohnya
      Jika Anda ingin memakai Rust, mungkin Anda membutuhkan performa tambahan, dan karena Anda sudah menerima bahasa yang kurang nyaman, Anda juga bisa menerima gaya yang kurang nyaman demi performa yang lebih baik
  • Saya belum pernah memakai Rust, tetapi saya memahami keluhannya
    Kalau kita harus menulis kode dengan cara khusus agar state bisa dipindahkan antar-thread demi redistribusi beban yang sebenarnya tidak diperlukan dan, saat CPU masih banyak menganggur, malah bisa memperlambat latensi end-to-end satu request, rasanya pasti menyebalkan
    Pendekatan seperti ini mungkin masuk akal pada platform yang state-nya secara default dapat dipindahkan dan hampir tidak pernah rusak, tetapi sepertinya tidak begitu di Rust
    Yang membuat saya penasaran adalah pengalaman pemakaiannya
    Saya ingin tahu apakah ini lebih dekat ke “kode tidak akan terkompilasi kalau tidak menambahkan mantra ajaib bernama Send”, atau lebih dekat ke “saat work stealing, state menjadi rusak lalu gagal secara sporadis dan sulit di-debug”

    • Misalnya kita menulis kode kira-kira seperti ini
      Ada Server, lalu di serve ia membaca pesan dan untuk tiap handler pesan menjalankan spawn(async move { ... }) sebagai task baru
      Awalnya semuanya berjalan baik
      Lalu suatu hari implementasi do_this diubah sehingga tipe this tidak lagi Send, dan spawn(...) pun menghasilkan error kompilasi mengerikan karena tipe yang dibuat oleh scope anonim async move { } bukan Send
      Alasannya belum tentu jelas, dan pesan error-nya juga tidak membantu
      Jika this bukan Send, ia tidak boleh ditahan melintasi .await dari do_that(arg).await
      Sebab setiap .await adalah titik eksekusi tempat future bisa yield dan dijadwalkan oleh executor ke thread lain
      Kalau tipenya bisa dibuat Send, tidak masalah
      Namun ada cukup banyak kasus ketika itu tidak bisa dilakukan, dan kalau begitu penjadwalan future harus diubah ke sesuatu seperti spawn_local
      Untuk memanggil ini, mungkin perlu menambahkan cukup banyak boilerplate
      Inilah masalah dengan Send
      Ini bukan sekadar soal menambahkan anotasi tipe; karena tidak selalu jelas apakah suatu tipe mengimplementasikan Send, hal ini bisa merembes secara halus ke dalam kode dan kemudian rusak dengan cara yang tidak jelas
    • Secara umum, compiler mendeteksi apakah sesuatu Send dan Sync
      Jika program ditulis secara thread-safe, tidak ada masalah
      Intinya ini
      Orang-orang mengeluh bahwa Tokio async sulit karena menuntut Send dan Sync di mana-mana, tetapi sebenarnya menulis kode konkurensi yang aman, apa pun jenisnya, memang sulit
      Ini tidak intuitif, dan masalahnya adalah async membuatnya terasa seolah-olah hal itu otomatis “ditangani”
      Namun kenyataannya tidak begitu
      Kita harus tahu apa yang sedang kita lakukan, dan compiler hanya membantu di sini
      Dengan thread-per-core, dalam beberapa situasi kebutuhan akan Send bisa disembunyikan, tetapi tidak semuanya
      Dan dalam jangka panjang, dari sisi arsitektur hal itu bisa kembali menggigit
    • Kode tidak akan terkompilasi kecuali semua yang diperlukan adalah Send/Sync di tempat yang membutuhkannya
      Saya bisa saja salah, tetapi cara malas untuk mencapainya biasanya adalah membungkus hal-hal yang bisa dibagikan dengan Arc atau Mutex
    • Menurut saya argumen untuk menolak thread-per-core sebagai default bisa dibuat sederhana
      Jika berorientasi CPU, work stealing kemungkinan besar lebih baik dalam sebagian besar kasus
      Jika berorientasi I/O, thread-per-core bisa bekerja lebih baik, tetapi karena CPU memang punya banyak ruang kosong, performa tidak terlalu penting
      Menurut saya work stealing adalah default yang lebih baik untuk dimasukkan ke API bahasa