Thread-per-core
(without.boats)- 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 + 'staticatauSend + 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_REUSEPORTmembagi 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
Sendpada 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
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 + 'staticakan terus memakainyaKebutuhan 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
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_...
Tulisan ini hanya membahas perdebatan lain, bukan perdebatan yang saya harapkan ditulis
Kita juga bisa membuat server ala Erlang yang memiliki tipe mutable dan berkomunikasi lewat channel, atau tetap memaksakan dengan
ArcRust memberi kekuatan untuk melakukan keduanya
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
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 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
ArcdanMutexbisa dihilangkanOverhead 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
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
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
Di API tokio namanya
LocalSethttps://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html...
Meski satu thread pada satu CPU sudah cukup, kita tetap bisa menginginkan concurrency
ArcdanMutex, kemungkinan akan memakaiRcdanRefCell, dan dari sisi kode rasanya sama-sama rumit dan verboseSaya 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 burukSend + Sync + 'static, membunuh kesenangan memakai Rust” terdengar terlalu melodramatisSaya tidak melihat menghilangkan
Send + Syncakan membuat perbedaan sebesar ituYang paling mengganggu adalah
'static, dan itu bukan karena work stealingYang 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 btSetidaknya 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
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/uniqMemang harus menaburkan
r.set_location(file!(), line!());secara manual sebelum setiap titikawait, 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(), dantask.rsmemilikilist_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
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
Akan disayangkan jika akibatnya kegunaan Rust untuk pemrograman sistem menjadi rusak
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 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
Sendagar task dapat dipindahkan antar-thread executor adalah cacat yang jelas dalam sistem async Rust itu sendiriSebab, bersama dengan masalah mendasar async Drop, hal itu menghalangi implementasi API berskop
Seperti halnya thread, seharusnya cukup jika batasan
Sendhanya ada pada fungsi seperti spawn atau pengiriman data melalui channelPendekatan 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
SendTulisan 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
Sendpada 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 sayaMasalah lifetime
'staticdan batasanSend/Syncsangat dirasakan oleh para pengembang, dan saya tidak mendapat kesan bahwa mereka bodohOrang-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
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
Sendatau tidakJadi 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
asyncitu sendiri merupakan optimisasi prematur99% 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 defaultAda 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”Ada
Server, lalu diserveia membaca pesan dan untuk tiap handler pesan menjalankanspawn(async move { ... })sebagai task baruAwalnya semuanya berjalan baik
Lalu suatu hari implementasi
do_thisdiubah sehingga tipethistidak lagiSend, danspawn(...)pun menghasilkan error kompilasi mengerikan karena tipe yang dibuat oleh scope anonimasync move { }bukanSendAlasannya belum tentu jelas, dan pesan error-nya juga tidak membantu
Jika
thisbukanSend, ia tidak boleh ditahan melintasi.awaitdarido_that(arg).awaitSebab setiap
.awaitadalah titik eksekusi tempat future bisa yield dan dijadwalkan oleh executor ke thread lainKalau tipenya bisa dibuat
Send, tidak masalahNamun ada cukup banyak kasus ketika itu tidak bisa dilakukan, dan kalau begitu penjadwalan future harus diubah ke sesuatu seperti
spawn_localUntuk memanggil ini, mungkin perlu menambahkan cukup banyak boilerplate
Inilah masalah dengan
SendIni 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 jelasSenddanSyncJika program ditulis secara thread-safe, tidak ada masalah
Intinya ini
Orang-orang mengeluh bahwa Tokio async sulit karena menuntut
SenddanSyncdi mana-mana, tetapi sebenarnya menulis kode konkurensi yang aman, apa pun jenisnya, memang sulitIni 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
Sendbisa disembunyikan, tetapi tidak semuanyaDan dalam jangka panjang, dari sisi arsitektur hal itu bisa kembali menggigit
Send/Syncdi tempat yang membutuhkannyaSaya bisa saja salah, tetapi cara malas untuk mencapainya biasanya adalah membungkus hal-hal yang bisa dibagikan dengan
ArcatauMutexJika 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