प्रति कोर थ्रेड (Thread-per-core)
(without.boats)- Rust async runtime की बहस “प्रति कोर OS thread” है या नहीं, इससे ज़्यादा इस चुनाव के बारे में है कि load को work-stealing से balance किया जाए या data movement घटाने के लिए share-nothing अपनाया जाए
- async Rust पहले से ही thread-per-core की मुख्य शर्तें—user-space concurrency handling और asynchronous I/O—पूरी करता है; बचा हुआ मुद्दा यह है कि cores के बीच state sharing को कितना टाला जा सकता है
- work-stealing threads के idle time को घटाकर tail latency और CPU utilization सुधारने की कोशिश करता है, लेकिन Rust में yield points के पार मौजूद state को thread-safe होना पड़ता है, इसलिए
Sendconstraint जुड़ता है - share-nothing data को किसी खास core के cache में बनाए रखकर latency घटा सकता है, लेकिन key-value store जैसी जगहों पर state partitioning आसान होती है, जबकि transactions और atomic changes की ज़रूरत वाले cases में कठिनाई काफी बढ़ जाती है
- Enberg paper share-nothing और shared-state की तुलना करता है, लेकिन दोनों में से कोई भी work-stealing का इस्तेमाल नहीं करता, इसलिए उसके results को सीधे Rust के work-stealing executors की आलोचना से जोड़ना मुश्किल है
Rust async runtime बहस का मूल मुद्दा
- Rust community में tokio जैसे प्रमुख async runtimes का default तौर पर multi-threaded executor इस्तेमाल करना, और कई tasks को work-stealing से dynamically balance करना, लगातार बहस का विषय रहा है
- कुछ users आलोचना करते हैं कि इस default की वजह से
Send + 'staticयाSend + Sync + 'staticconstraints coding experience को खराब करते हैं - कुछ लोग single-threaded server पसंद करते हैं, लेकिन यहाँ मानकर चला गया है कि Rust system एक या अधिक CPU core time इस्तेमाल करना चाहता है
- विकल्प के तौर पर सुझाया जाने वाला “thread-per-core” तेज़ और implement करने में आसान होने की उम्मीद जगाता है, लेकिन performance और implementation ease दोनों साथ पाना कठिन है
“thread-per-core” नाम से पैदा होने वाला भ्रम
- मौजूदा multi-threaded async executors भी व्यापक अर्थ में thread-per-core ही हैं
- वे प्रति core OS thread बनाते हैं
- और cores की संख्या से कहीं ज़्यादा tasks को उन threads पर schedule करते हैं
- Pekka Enberg thread-per-core को तीन ideas के combination के रूप में समझाते हैं
- महंगे kernel threads के बजाय user space में concurrency handle करना
- per-core threads block न हों, इसलिए asynchronous I/O इस्तेमाल करना
- synchronization cost और CPU caches के बीच data movement हटाने के लिए data को CPU cores के बीच partition करना
- Enberg के मुताबिक high-throughput systems बनाने के लिए पहली दो बातें महत्वपूर्ण हैं, लेकिन तीसरी केवल बहुत बड़े multicore machines पर ही ज़रूरी हो सकती है
- अगर आप async Rust इस्तेमाल कर रहे हैं, तो user-space concurrency और asynchronous I/O वाली पहली दो शर्तें पहले से पूरी हैं
- इसलिए बहस का केंद्र thread-per-core अपने आप में नहीं, बल्कि यह है कि work-stealing और share-nothing में से कौन-सा optimization चुना जाए
work-stealing का उद्देश्य और लागत
- work-stealing एक optimization है जो सभी threads को लगातार काम देते रहने की कोशिश करके tail latency घटाता है
- वास्तविक systems में हर task के लिए ज़रूरी work अलग-अलग होता है
- एक HTTP request दूसरे request से कहीं ज़्यादा काम मांग सकता है
- शुरुआत में threads के बीच work evenly बाँट भी दिया जाए, तो unpredictable task differences के कारण समय के साथ workload बदल सकता है
- peak load पर किसी thread को उसकी processing capacity से ज़्यादा काम मिल सकता है, जबकि दूसरे threads idle रह सकते हैं
- tokio, async-std और smol सभी tail latency घटाने और CPU utilization सुधारने के लक्ष्य से work-stealing implement करते हैं
- लागत यह है कि task एक thread पर रुकने के बाद किसी दूसरे thread पर फिर शुरू हो सकता है
- yield points के पार इस्तेमाल होने वाली state thread-safe होनी चाहिए
- Rust API में यह future के
Sendहोने की requirement के रूप में दिखता है - system state साफ़ तौर पर समझ में न हो, तो
Sendकैसे guarantee किया जाए यह तय करना मुश्किल होता है
- जब state दूसरे thread पर move होती है, तो synchronization cost और cache misses पैदा होते हैं, और यह हर CPU के सिर्फ अपनी state संभालने वाले share-nothing principle से टकराता है
share-nothing की performance logic
- share-nothing एक design है जो data को कई cores द्वारा shared धीमे cache में रखने के बजाय किसी खास CPU core के तेज़ cache में बनाए रखकर tail latency घटाने की कोशिश करता है
- Enberg का paper The Impact of Thread-Per-Core Architecture on Application Tail Latency share-nothing key-value store और shared-state architecture वाले memcached को benchmark करके tail latency improvement दिखाता है
- Rust community में इस paper को “71% performance improvement” की तरह simplistically quote करना सतही और कम उपयोगी है
- Enberg का key-value store share-nothing architecture के लिए state और connections को बाँटता है
- hash function से keyspace को कई threads में partition करता है
SO_REUSEPORTसे incoming TCP connections को threads के बीच बाँटता है- connection manage करने वाले thread से संबंधित keyspace manage करने वाले thread तक requests को message-passing channel से route करता है
- memcached में सभी threads partitioned keyspace का ownership share करते हैं, और हर partition mutex से protected होता है
- Enberg के results दिखाते हैं कि mutex के बजाय channels इस्तेमाल करने वाली architecture कम tail latency हासिल कर सकती है
- इसे इस तरह देखा जा सकता है कि वही partition बार-बार access होता है और एक core के cache में बना रहता है, जिससे cache misses घटते हैं
- हालांकि advanced kernel features और carefully planned architecture से data movement टालने वाला design, data को mutex के अंदर wrap करने वाले तरीके से implement करने में आसान है—ऐसा मानना मुश्किल है
जहाँ state partitioning आसान है और जहाँ कठिन
- key-value store ऐसा case है जहाँ application state को कई threads में बाँटना आसान होता है, इसलिए यह share-nothing के लिए अच्छी तरह fit बैठता है
- अधिक complex applications में अगर कई partitions की state को transactionally या atomically बदलना पड़े, तो सही implementation के लिए कहीं ज़्यादा सावधानी चाहिए
- share-nothing की advocacy कुछ हद तक पुराने दौर में eventual consistency databases को लेकर हुई hype जैसी लगती है
- performance बेहतर हो सकती है
- लेकिन data inconsistency से पैदा होने वाले bugs से बचने के लिए careful design ज़रूरी है
Enberg paper और Rust work-stealing बहस के बीच दूरी
- Enberg की implementation और memcached दोनों work-stealing का इस्तेमाल नहीं करते
- इसी वजह से paper के मुख्य performance results को Rust के work-stealing architecture से सीधे जोड़ना मुश्किल है
- Enberg की architecture में work-stealing जोड़ने से data movement कुछ बढ़ सकता है, लेकिन यह CPU utilization बढ़ाने का तरीका भी हो सकता है
- memcached में work-stealing जोड़ने पर वह मददगार क्यों नहीं होगा, यह कल्पना करना कठिन माना गया है
- paper की implementation keyspace के balanced partitioning और
SO_REUSEPORTके जरिए work को पहले से evenly बाँटने के लिए design की गई है - real-world environments में dynamic imbalance दिख सकता है
- hot key ज्यादा reads और writes लेकर उस keyspace को manage करने वाले thread का workload बढ़ा देता है
- कुछ connections दूसरे connections से ज़्यादा requests करते हैं, जिससे उन connections को manage करने वाले thread का workload बढ़ता है
- समझा जाता है कि paper के benchmarks में हर connection random keys पर fixed amount of work करता है, इसलिए ऐसे imbalance conditions reproduce नहीं हुए
- hot key को extra partitions में cache करने जैसे तरीकों से share-nothing system के अंदर भी imbalance कम करने वाले designs संभव हैं
- कुछ tasks को state movement टालने के लिए specific core पर pin किया जा सकता है, जबकि optimization के रूप में work-stealing के दूसरे रूप भी इस्तेमाल किए जा सकते हैं
व्यावहारिक निष्कर्ष
- अगर system को CPU caches के बीच data movement टालने के लिए carefully design किया जाए, तो वह ऐसा न करने की तुलना में बेहतर performance दे सकता है
- लेकिन अगर
Sendconstraint को generics में जोड़ना ही सबसे बड़ी शिकायत है, तो यह मानना मुश्किल है कि आप उस तरह की fine-grained performance engineering कर रहे हैं - अगर system वैसे भी shared state इस्तेमाल करने वाला है, तो load की स्थिति में work-stealing CPU utilization नहीं सुधारेगा—ऐसा मानना मुश्किल है
1 टिप्पणियां
Hacker News की राय
व्यक्तिगत रूप से यह लेख ऐसा लगता है कि बड़ी तस्वीर छोड़कर बारीकियों में उलझ जाता है
बहस का मुख्य मुद्दा core-per-thread work-stealing executor नहीं, बल्कि यह है कि Rust में async/await उसके लिए अच्छा abstraction है या नहीं
जितना ज़्यादा async code लिखता हूं, उतना ही लगता है कि abstraction leak करता है और उसके विरुद्ध programming करना मुश्किल है
लोग जिस वैकल्पिक concurrency model को चाहते हैं, वह work-stealing executor के ऊपर stacked coroutines और channels के जरिए structured concurrency है
जब तक कोई उसे implement करके async/await और future-based model से तुलना नहीं करता, productive चर्चा मुश्किल है; async नापसंद करने वाले लोग इससे बचेंगे, और जिन्हें
Send + Sync + 'staticकी परवाह नहीं है वे इसे इस्तेमाल करते रहेंगेऐसा scale जिन कामों में चाहिए होता है, वे ज्यादातर मामलों में बहुत दुर्लभ लगते हैं
ऐसे लेख बस “kernel threads महंगे होते हैं” कहकर आगे बढ़ जाते हैं, जैसे यह मूल रूप से सच हो, लेकिन असल में ऐसा नहीं है
अगर काम केवल no-op tasks लगातार create करना नहीं है, तो “real threads” का overhead कम होने की संभावना है, और बदले में मिलने वाली simplicity बहुत बड़ी है
withoutboats ने जिस उद्धरण को उठाया, वह linked लेख में default multi-threading और work stealing की खास तौर पर आलोचना वाला हिस्सा है
[1] https://www.reddit.com/r/rust/comments/16p47f1/the_state_of_...
यह लेख बस एक अलग बहस को संबोधित करता है, वह बहस नहीं जिसे मैं चाहता था कि यह लिखता
आप mutable type को own करके channels से communicate करने वाला Erlang-style server भी बना सकते हैं, या
Arcके साथ आगे बढ़ सकते हैंRust आपको दोनों करने की ताकत देता है
stacked coroutines उस point पर ज्यादा मायने नहीं रखते, क्योंकि तब आप बस अलग threads इस्तेमाल कर सकते हैं
करीब 15 साल पहले core-per-thread model जिस मूल समस्या को हल करने आया था, वह general-purpose multi-core servers पर compute scalability और efficiency हासिल करना था
कुछ दावों के उलट, core-per-thread स्पष्ट रूप से CPU-centric workloads को optimize करने के लिए था, और बाद में अधिक sophisticated I/O handling की जरूरत जरूर पड़ी, लेकिन यह high-throughput I/O-centric workloads में भी बेहतरीन साबित हुआ
इस तरह के लेख देखकर लगता है कि core-per-thread architecture अपनाते समय हुई कई software design गलतियों को तेजी से दोहराया जा रहा है
निष्पक्ष रूप से कहें तो core-per-thread से जुड़ी computer science मुख्यतः HPC से आई है और उसका documentation कम है
यह लेख core-per-thread architecture की कठिन समस्या, यानी cores के बीच work balancing, पर केंद्रित है
मूल model data/load के push/pull के चार प्रकार हैं, और work stealing मूल रूप से load pull model है
इस तरीके का overhead तभी कम होता है जब इसकी जरूरत लगभग न पड़े, यानी जब natural balance हो, जो वास्तविक समस्याओं में दुर्लभ है
cores के बीच dynamic load skew आम होने वाले ज्यादा दिलचस्प workloads में coordination overhead के कारण work stealing performance bottleneck बन जाता है
फिर भी इसे समझना आसान है, इसलिए सही workloads के लिए यह अब भी इस्तेमाल होता है, लेकिन generalize अच्छी तरह नहीं होता
लेख में जिन rare workloads का उल्लेख नहीं है, उनमें शायद कुछ cases में यह सबसे अच्छा भी हो सकता है
आजकल सबसे ज्यादा traction पाने वाला model data push लगता है; यह कम intuitive है, लेकिन threads के बीच काफी कम coordination चाहिए
इस model के लिए भी कुछ workloads fit नहीं होते, लेकिन आम तौर पर मिलने वाले ज्यादातर workloads में यह अच्छी तरह generalize होता है
core-per-thread architecture बना रहेगा। scalability और efficiency में इसे हराना मुश्किल है
हालांकि modern और idiomatic core-per-thread design कैसा दिखता है, इस पर ज्यादातर software engineers की intuition कम है, और इस विषय को गहराई से कवर करने वाले articles या papers कम होने से स्थिति और खराब हो जाती है
profile की bio और इस explanation को देखकर लगता है कि ऐसी applications शायद वे मुख्य type नहीं हैं जिनसे आपका वास्ता पड़ता है
अगर संबंधित literature के links हों तो देखना चाहूंगा
“I/O-केंद्रित कहने का मतलब है कि Rust में लिखने पर असल में इतना काम नहीं है कि एक ही core को saturate कर दे। तो जाहिर है, single-threaded system लिखिए” — इस वाक्य से मैं सहमत हूँ
मेरी कई applications background में events पर react करने वाले daemons हैं, और उन्हें single-threaded बनाने से
ArcऔरMutexका overhead हटाया जा सकता हैयह overhead उस समय ज़्यादातर syntactic burden जैसा ही होता है, लेकिन debugging और maintenance आसान हो जाते हैं
Rust में मुझे यही बात पसंद है कि आप सिर्फ़ उसी चीज़ की कीमत चुकाते हैं जिसकी ज़रूरत है
जिस मूल लेख का यह लेख जवाब दे रहा है, वह tokio और दूसरी async libraries की आलोचना करता है कि वे साधारण single-threaded architecture पर लौटना मुश्किल बना देती हैं
इसमें कुछ अतिशयोक्ति है, लेकिन कुल मिलाकर मैं उस आलोचना से सहमत हूँ
high-throughput applications के लिए बेहतर होने के कारण defaults को और complex बनाना Rust के ideals के उलट लगता है
वे throughput से बंधी नहीं होतीं; ज़्यादातर idle रहती हैं और काम आते ही system resource usage कम रखने के लिए जल्दी खत्म करने की कोशिश करती हैं
जब तक कभी-कभार बहुत बड़ा workload spike न आए और उस समय latency बहुत महत्वपूर्ण न हो, एक से ज़्यादा threads इस्तेमाल करने से फायदा नहीं मिलता, सिर्फ़ complexity और overhead बढ़ते हैं
operating system में हर system service को IPC requests concurrent तरीके से handle करना पड़ता है, लेकिन ज़्यादातर कुल CPU consumption घटाने के लिए single thread पर handle करती हैं
4-core device पर दर्जनों services को thread-per-core बनाना CPU और RAM की बर्बादी है
tokio API में इसे
LocalSetकहा जाता हैhttps://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html...
single CPU के single thread से काम चल जाने पर भी आप फिर भी concurrency चाह सकते हैं
ArcऔरMutexकी जगह आपRcऔरRefCellइस्तेमाल करेंगे, लेकिन code के लिहाज़ से यह लगभग उतना ही complex और verbose नहीं होगा क्या?efficiency कम होती है, यह समझता हूँ, लेकिन जिस case का वर्णन किया गया है, उसमें कुछ extra atomic operations की लागत वैसे भी नगण्य नहीं होगी क्या?
quote की बात करें तो, “Rust async programming का मूल पाप यह था कि default को multi-threaded बना दिया गया…
Send + 'static, और उससे भी बदतरSend + Sync + 'static, Rust इस्तेमाल करने का मज़ा खत्म कर देता है” — यह expression बहुत melodramatic लगता है, इससे मैं सहमत हूँSend + Syncहटाने से इतना बड़ा फर्क पड़ेगा, मुझे ऐसा नहीं लगतासबसे ज़्यादा परेशान करने वाली चीज़
'staticहै, और वह work stealing की वजह से नहीं हैजो चाहिए वह scoped concurrency है
उदाहरण के लिए <https://github.com/tokio-rs/tokio/issues/2596> जैसी चीज़
अभी Rust async में एक और चीज़ जो मुझे सच में नापसंद है, वह है instrumentation की कमी
कंपनी में एक production issue है जहाँ कुछ tasks बस रुक जाते हैं, और मैं
gdb; thread apply all btजैसा कुछ करना चाहता हूँकम से कम उम्मीद है कि <https://github.com/tokio-rs/tokio/issues/5638> आ जाए
अभी भी यह experimental रूप में मौजूद है, लेकिन मेरे अनुभव में कभी-कभी panic हो जाता है
आज असल में एक PR लिखकर SIGTERM पर experimental version इस्तेमाल करके देखने वाला हूँ
वैसे भी shutdown चल रहा होगा, इसलिए crash हो भी जाए तो फर्क नहीं पड़ता — यही निर्णय है
इन शिकायतों में से कोई भी work stealing हटाने से हल नहीं होती
और भी सूची बनाऊँ तो भी work stealing हटाने से लगभग कुछ भी मदद नहीं मिलेगी
debugger की भी ज़रूरत नहीं; JVM को SIGQUIT भेजिए, वह सभी threads के stack traces stderr पर dump कर देता है और चलना जारी रखता है
इसमें यह भी शामिल होता है कि हर thread कौन सा lock पकड़े हुए है या किसका इंतज़ार कर रहा है
हर बार जब कोई दूसरी language इस्तेमाल करता हूँ, तो यह feature याद आता है
इसे production में temporary profiling के लिए भी इस्तेमाल कर सकते हैं
कई snapshots लेने के बाद
grep/sed/sort/uniqसे hotspots ढूँढ लेंहर
awaitpoint से पहले manuallyr.set_location(file!(), line!());डालना पड़ता है, लेकिन system क्यों रुका हुआ लगता है, यह समझाने में इससे कई बार मदद मिली है[1] https://github.com/antialize/tokio-tasks/blob/main/src/run_t... में
set_location()है, औरtask.rsमेंlist_tasks()हैइस समस्या का कोई एक सही जवाब नहीं है; सब कुछ use case पर निर्भर करता है
आखिरकार यह I/O-केंद्रित और CPU-केंद्रित workloads का मामला है, और cache eviction या lock contention जैसी चीज़ें कितनी बुरी तरह असर डालती हैं, इसका भी
अगर आप किसी साझा virtual server पर ऐसा HTTP server चला रहे हैं जो बाहरी database से बात करता है और बीच में हल्का business logic है, तो work stealing और thread reuse intuitively सही लगते हैं
बेशक, हमेशा benchmark करना चाहिए
इसके उलट, अगर आप database या वैसी ही कोई system बना रहे हैं, और load के तहत high concurrency और बहुत सारे context switches हर जगह cache eviction और contention पैदा कर रहे हैं, तो मुश्किल होगी
ऐसी स्थिति में thread-per-core बहुत वाजिब है, और async framework खुद भी शायद मायने न रखे
क्या “बेहतर” है, इसका कोई dogmatic सही जवाब नहीं है
आपको application को profile करना चाहिए
जैसा पहले भी कहा, मुझे लगता है कि Rust का पूरा focus web service development की भारी आमद से विकृत हो रहा है
Rust उस काम के लिए सही भाषा है या नहीं, इस पर मुझे अभी भी भरोसा नहीं है, लेकिन उनके लिए यह काफ़ी ठीक काम करती दिखती है, तो ठीक है
पर भाषा पर public discussion और सामने धकेले जा रहे crates अभी कुल मिलाकर इसी bias को दिखाते हैं
यह इस forum के कई software engineers का bias भी है
game engines, cryptography libraries, kernels, command-line tools, compilers जैसी system applications async को छुए बिना भी सफलतापूर्वक बनाई जा रही हैं
मैं एक बड़ी cryptography library maintain करता हूँ, और async वाली चीज़ों से बिल्कुल प्रभावित नहीं हुआ
अगर इसके नतीजे में Rust की systems programming में उपयोगिता खराब होती है, तो अफ़सोस होगा
application में खुद request के बाहर state बहुत कम होती है, और socket listeners तथा database connections को per-thread बाँटा जा सकता है
जो state बचती है, वह भी requests के बीच ज़्यादातर static होने की संभावना है, इसलिए cache invalidation बार-बार नहीं होगा
shared state न होने से ownership संभालना भी आसान होना चाहिए
मैं यह नहीं कह रहा कि यह अच्छा है या बुरा, लेकिन अब Rust को अलग-अलग quality वाली web-related libraries और frameworks की अंतहीन धारा के साथ जीना होगा
और क्योंकि कई core infrastructure libraries और crates ने async-first approach चुनी है, async चर्चा का विषय बना रहेगा
अब हालत यह है कि जब तक कोई आम developer project की प्रमुख policy के तौर पर async न इस्तेमाल करने का फैसला न करे, business problems के लिए साधारण synchronous code लिखना मुश्किल हो गया है
executor threads के बीच tasks को move करने की अनुमति देने के लिए
Sendbound मांगना मुझे Rust async system की साफ़ कमी लगता हैक्योंकि async Drop की मूल समस्या के साथ यह scoped API लागू करने से रोकता है
threads की तरह,
Sendbound सिर्फ़ उन functions पर होना पर्याप्त होना चाहिए जो spawn या channels के ज़रिए data transfer करती हैंnon-shared approach आम तौर पर इस कमी को छिपाने के workaround से ज़्यादा कुछ नहीं है
task को किसी खास thread/core पर वैकल्पिक रूप से pin करना फायदेमंद है और कुछ स्थितियों में सच में उपयोगी है, लेकिन वह ज़्यादा सूक्ष्म चर्चा है और
Sendसे जुड़ी async users की शिकायतों से उसका बहुत संबंध नहीं हैअच्छा लेख है, और सलाह दूँगा कि सिर्फ़ title देखकर न रुकें, आगे पढ़ें
जो वाक्य मुझे सबसे पसंद आया वह था: “अगर सबसे बड़ी शिकायत किसी generic में
Sendbound जोड़ने की है, तो विश्वास करना मुश्किल है कि वह व्यक्ति उस स्तर की engineering कर रहा है”edit: “duped” की comment से पूरी तरह सहमत हूँ
इस चर्चा का बड़ा context मुझे नहीं पता था, इसलिए शायद मैंने यह वाक्य बहुत जल्दबाज़ी में quote कर दिया
“जो लोग system state को ठीक से नहीं देख पाते, उन्हें future को
Sendबनाना सुनिश्चित करने का सबसे अच्छा तरीका ढूँढना मुश्किल लग सकता है” वाला हिस्सा थोड़ा घमंडी नहीं सुनाई देता क्या?'staticlifetime औरSend/Syncconstraints की समस्याएँ developers को व्यापक रूप से महसूस होती हैं, और मुझे ऐसा impression नहीं मिला कि वे बेवकूफ़ हैंलोग कहते हैं कि work stealing न करना आसान और तेज़ है
मेरा तर्क है कि यह दोनों में से एक है
non-shared तरीका तेज़ होना है तो code को ऐसे design करना पड़ेगा जो shared-state architecture को thread-safe बनाने से आसान नहीं है
अगले paragraph में “slow” के जवाब में parallel sentence है
मैं यह नहीं सोचता कि parallel/concurrent Rust को compile कराने में कठिनाई झेलने वाले लोग बेवकूफ़ हैं
बस मुझे यह पसंद नहीं कि वे ऐसे behave करें जैसे हमने उनके लिए बनाई API ने उनकी ज़िंदगी खराब कर दी हो
अगर वह काम वैसे भी करना है, तो वह ज़्यादा मुश्किल नहीं है
बात “लोग छोटी चीज़ पर overreact करते हैं” से ज़्यादा यह है कि async उस समस्या को पहले हल करवाता है जिसे कभी न कभी हल करना ही था
borrow checker जैसा ही अहसास है
कभी-कभी यह ज़रूरत से ज़्यादा restrictive होता है, लेकिन कभी-कभी जब आप मान लेते हैं कि सब ठीक है, तब कोई corner case सच में मौजूद होता है जिस पर आपने विचार नहीं किया था
Sendहै या नहींइसलिए quote किया गया वाक्य मुझे समझ में आता है
यह लेख बड़ी तस्वीर छोड़कर details में उलझता है
हर program में best performance पाने का कोई हमेशा सही तरीका नहीं है
बहस जितनी चाहे कर सकते हैं, लेकिन thread-per-core के pros और cons classic “यह परिस्थिति पर निर्भर करता है” वाली बात हैं
समस्या असल में यह है कि
asyncका इस्तेमाल ही premature optimization हैRust programs के 99% redis या linkerd जैसे नहीं हैं
वे CLI tools या web apps हैं जो Python या Ruby में लिखे जाएँ तो भी पर्याप्त तेज़ होंगे
फिर community ने blocking I/O Rust को क्यों छोड़ दिया और सब कुछ async क्यों हो गया, और developers default की तरह हर चीज़ पर
#[tokio::main]क्यों लगा रहे हैं, यह समझ नहीं आताऐसी भाषाएँ हैं जिनका programming experience बेहतर है और जो speed के बदले यह देती हैं; Python इसका उदाहरण है
अगर आप Rust इस्तेमाल करना चाहते हैं तो शायद आपको extra performance चाहिए, और जब आप पहले ही कम convenient भाषा स्वीकार कर चुके हैं, तो बेहतर performance के लिए कम convenient style भी स्वीकार कर सकते हैं
मैंने Rust इस्तेमाल नहीं किया है, लेकिन शिकायत समझ आती है।
अगर state को threads के बीच move किया जा सके, इसके लिए code को खास तरीके से लिखना पड़े—सिर्फ ऐसे load redistribution के लिए जो ज़रूरी भी नहीं है और जब CPU headroom काफी हो तो किसी single request की end-to-end latency को उल्टा बढ़ा भी सकता है—तो यह परेशान करने वाला लगेगा।
अगर कोई ऐसा platform हो जहां movable state default हो और लगभग कभी टूटता न हो, तो यह approach समझ में आ सकती है, लेकिन Rust में ऐसा लगता नहीं है।
मुझे असली जिज्ञासा इसके इस्तेमाल के अनुभव को लेकर है।
क्या यह बस “जब तक
Sendनाम का magic spell नहीं जोड़ोगे, code compile नहीं होगा” जैसा है, या “work stealing के दौरान state खराब हो जाती है और बीच-बीच में, debug करने में मुश्किल तरीके से fail होता है” जैसा?एक
Serverहै, औरserveमें message पढ़ने के बाद हर message handler को नई task के रूप मेंspawn(async move { ... })किया जाता है।शुरुआत में सब ठीक चलता है।
फिर एक दिन आप
do_thisका implementation बदलते हैं, जिससेthisका type अबSendनहीं रह जाता, औरspawn(...)पर आपको एक डरावना compile error मिलता है कि anonymousasync move { }scope ने जो type बनाया है वहSendनहीं है।वजह हमेशा साफ नहीं होती, और error message भी मददगार नहीं होता।
अगर
thisSendनहीं है, तो उसेdo_that(arg).awaitके.awaitके पार पकड़े नहीं रखा जा सकता।क्योंकि हर
.awaitएक ऐसा execution point है जहां future yield कर सकता है और executor उसे किसी दूसरे thread पर schedule कर सकता है।अगर आप type को
Sendबना सकते हैं तो ठीक है।लेकिन कई बार ऐसा संभव नहीं होता, और तब future scheduling को
spawn_localजैसी चीज़ में बदलना पड़ता है।इसे call करने के लिए काफी boilerplate जोड़ना पड़ सकता है।
यही
Sendकी समस्या है।यह सिर्फ type annotation जोड़ने का मामला नहीं है; क्योंकि कोई type
Sendimplement करता है या नहीं, यह हमेशा स्पष्ट नहीं होता, इसलिए यह code में सूक्ष्म रूप से घुस जाता है और बाद में अस्पष्ट तरीके से टूट सकता है।SendऔरSyncहै या नहीं।अगर आप program को thread-safe तरीके से लिखते हैं, तो दिक्कत नहीं होती।
मुख्य बात यही है।
लोग शिकायत करते हैं कि Tokio async हर जगह
SendऔरSyncमांगता है, इसलिए मुश्किल है, लेकिन असल में किसी भी तरह का safe concurrency code लिखना ही मुश्किल है।यह intuitive नहीं है, और समस्या यह है कि async ऐसा महसूस कराता है जैसे वह अपने-आप इसे “संभाल” लेगा।
लेकिन असल में ऐसा नहीं है।
आपको पता होना चाहिए कि आप क्या कर रहे हैं, और compiler यहां बस मदद करता है।
thread-per-core पर जाने से कुछ स्थितियों में
Sendकी ज़रूरत छिप सकती है, लेकिन सभी में नहीं।और लंबे समय में architecture के स्तर पर यह वापस आकर नुकसान पहुंचा सकता है।
Send/Syncनहीं है तो code compile नहीं होगा।मैं गलत भी हो सकता हूं, लेकिन इसे हासिल करने का lazy तरीका आमतौर पर जिन चीज़ों को share किया जा सकता है उन्हें
ArcयाMutexमें wrap करना होता है।अगर workload CPU-centric है, तो work stealing ज्यादातर मामलों में बेहतर होगा।
अगर I/O-centric है, तो thread-per-core बेहतर काम कर सकता है, लेकिन वैसे भी CPU headroom काफी होता है, इसलिए performance बहुत मायने नहीं रखती।
मेरे हिसाब से work stealing, language API में रखने के लिए बेहतर default है।