1 पॉइंट द्वारा GN⁺ 2023-10-08 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • 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 होना पड़ता है, इसलिए Send constraint जुड़ता है
  • 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 + 'static constraints 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 दे सकता है
  • लेकिन अगर Send constraint को generics में जोड़ना ही सबसे बड़ी शिकायत है, तो यह मानना मुश्किल है कि आप उस तरह की fine-grained performance engineering कर रहे हैं
  • अगर system वैसे भी shared state इस्तेमाल करने वाला है, तो load की स्थिति में work-stealing CPU utilization नहीं सुधारेगा—ऐसा मानना मुश्किल है

1 टिप्पणियां

 
GN⁺ 2023-10-08
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 की परवाह नहीं है वे इसे इस्तेमाल करते रहेंगे

    • तो अगर सचमुच C10M scale की जरूरत नहीं है, तो बस connection-per-thread इस्तेमाल करके और ज्यादा जटिल चीज़ें न अपनाना बेहतर नहीं होगा?
      ऐसा scale जिन कामों में चाहिए होता है, वे ज्यादातर मामलों में बहुत दुर्लभ लगते हैं
      ऐसे लेख बस “kernel threads महंगे होते हैं” कहकर आगे बढ़ जाते हैं, जैसे यह मूल रूप से सच हो, लेकिन असल में ऐसा नहीं है
      अगर काम केवल no-op tasks लगातार create करना नहीं है, तो “real threads” का overhead कम होने की संभावना है, और बदले में मिलने वाली simplicity बहुत बड़ी है
    • मुझे async/await बनाम stacked coroutines की बहस ज्यादा दिलचस्प लगती है, लेकिन इस लेख की बहस वह नहीं है
      withoutboats ने जिस उद्धरण को उठाया, वह linked लेख में default multi-threading और work stealing की खास तौर पर आलोचना वाला हिस्सा है
      [1] https://www.reddit.com/r/rust/comments/16p47f1/the_state_of_...
    • दुनिया में ऐसे कई विषय हैं जिन पर लोग सहमत नहीं हैं
      यह लेख बस एक अलग बहस को संबोधित करता है, वह बहस नहीं जिसे मैं चाहता था कि यह लिखता
    • async/await को channels के साथ इस्तेमाल करते हुए, async function arguments के रूप में केवल immutable references या copy types पास करने और mutable shared types को channels के जरिए communicate करने तक सीमित किया जा सकता है
      आप mutable type को own करके channels से communicate करने वाला Erlang-style server भी बना सकते हैं, या Arc के साथ आगे बढ़ सकते हैं
      Rust आपको दोनों करने की ताकत देता है
    • मेरी समझ में Rust के मौजूदा async support के ऊपर channels इस्तेमाल करने से रोकने वाली कोई चीज़ नहीं है
      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 कम होने से स्थिति और खराब हो जाती है

    • Rust async framework के work stealing इस्तेमाल करने की मुख्य वजह शायद यह है कि framework level पर इसे enable करना आसान है और यह कई applications, खासकर ideal तरीके से design न की गई applications, में performance सुधारता है
      profile की bio और इस explanation को देखकर लगता है कि ऐसी applications शायद वे मुख्य type नहीं हैं जिनसे आपका वास्ता पड़ता है
      अगर संबंधित literature के links हों तो देखना चाहूंगा
    • data push का idea क्या है, यह जानने की उत्सुकता है
  • “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 के उलट लगता है

    • मैंने ऐसी services लिखी हैं, लेकिन उन्हें I/O-केंद्रित नहीं कहा होता
      वे throughput से बंधी नहीं होतीं; ज़्यादातर idle रहती हैं और काम आते ही system resource usage कम रखने के लिए जल्दी खत्म करने की कोशिश करती हैं
      जब तक कभी-कभार बहुत बड़ा workload spike न आए और उस समय latency बहुत महत्वपूर्ण न हो, एक से ज़्यादा threads इस्तेमाल करने से फायदा नहीं मिलता, सिर्फ़ complexity और overhead बढ़ते हैं
    • सही है। concurrency चाहने वाली हर चीज़ web server नहीं होती
      operating system में हर system service को IPC requests concurrent तरीके से handle करना पड़ता है, लेकिन ज़्यादातर कुल CPU consumption घटाने के लिए single thread पर handle करती हैं
      4-core device पर दर्जनों services को thread-per-core बनाना CPU और RAM की बर्बादी है
    • tokio सच में ज़रूरत पड़ने पर इस्तेमाल करने के लिए single-threaded executor support करता है, और यह मुश्किल भी नहीं है
      tokio API में इसे LocalSet कहा जाता है
      https://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html...
    • मैं भी उसी quote पर लिखने वाला था
      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 हटाने से लगभग कुछ भी मदद नहीं मिलेगी

    • Java की बहुत आलोचना होती है, लेकिन यह हिस्सा लगभग 25 साल पहले से ही ठीक-ठाक काम करता आया है और problems solve करने में बेहद useful रहा है
      debugger की भी ज़रूरत नहीं; JVM को SIGQUIT भेजिए, वह सभी threads के stack traces stderr पर dump कर देता है और चलना जारी रखता है
      इसमें यह भी शामिल होता है कि हर thread कौन सा lock पकड़े हुए है या किसका इंतज़ार कर रहा है
      हर बार जब कोई दूसरी language इस्तेमाल करता हूँ, तो यह feature याद आता है
      इसे production में temporary profiling के लिए भी इस्तेमाल कर सकते हैं
      कई snapshots लेने के बाद grep/sed/sort/uniq से hotspots ढूँढ लें
    • ऐसे issues को कम करने के लिए कंपनी में हम एक library इस्तेमाल करते हैं जो periodically log करती है कि कौन से tasks चल रहे हैं और हर task अभी किस file/line number पर है
      हर await point से पहले manually r.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 भी है

    • async का Rust के दूसरे हिस्सों पर असर पड़ता हुआ मैंने ज़्यादा नहीं देखा
      game engines, cryptography libraries, kernels, command-line tools, compilers जैसी system applications async को छुए बिना भी सफलतापूर्वक बनाई जा रही हैं
      मैं एक बड़ी cryptography library maintain करता हूँ, और async वाली चीज़ों से बिल्कुल प्रभावित नहीं हुआ
    • web developers की आमद वाली बात से सहमत हूँ
      अगर इसके नतीजे में Rust की systems programming में उपयोगिता खराब होती है, तो अफ़सोस होगा
    • मुझे तो लगा था कि web apps उलटे thread-per-core के लिए ideal हैं
      application में खुद request के बाहर state बहुत कम होती है, और socket listeners तथा database connections को per-thread बाँटा जा सकता है
      जो state बचती है, वह भी requests के बीच ज़्यादातर static होने की संभावना है, इसलिए cache invalidation बार-बार नहीं होगा
      shared state न होने से ownership संभालना भी आसान होना चाहिए
    • मेरे हिसाब से यह इसलिए हुआ क्योंकि Rust community ने market और visibility पाने के लिए ऐसे developers को काफ़ी सक्रिय रूप से आकर्षित किया
      मैं यह नहीं कह रहा कि यह अच्छा है या बुरा, लेकिन अब 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 करने की अनुमति देने के लिए Send bound मांगना मुझे Rust async system की साफ़ कमी लगता है
    क्योंकि async Drop की मूल समस्या के साथ यह scoped API लागू करने से रोकता है
    threads की तरह, Send bound सिर्फ़ उन functions पर होना पर्याप्त होना चाहिए जो spawn या channels के ज़रिए data transfer करती हैं
    non-shared approach आम तौर पर इस कमी को छिपाने के workaround से ज़्यादा कुछ नहीं है
    task को किसी खास thread/core पर वैकल्पिक रूप से pin करना फायदेमंद है और कुछ स्थितियों में सच में उपयोगी है, लेकिन वह ज़्यादा सूक्ष्म चर्चा है और Send से जुड़ी async users की शिकायतों से उसका बहुत संबंध नहीं है

  • अच्छा लेख है, और सलाह दूँगा कि सिर्फ़ title देखकर न रुकें, आगे पढ़ें
    जो वाक्य मुझे सबसे पसंद आया वह था: “अगर सबसे बड़ी शिकायत किसी generic में Send bound जोड़ने की है, तो विश्वास करना मुश्किल है कि वह व्यक्ति उस स्तर की engineering कर रहा है”
    edit: “duped” की comment से पूरी तरह सहमत हूँ
    इस चर्चा का बड़ा context मुझे नहीं पता था, इसलिए शायद मैंने यह वाक्य बहुत जल्दबाज़ी में quote कर दिया

  • “जो लोग system state को ठीक से नहीं देख पाते, उन्हें future को Send बनाना सुनिश्चित करने का सबसे अच्छा तरीका ढूँढना मुश्किल लग सकता है” वाला हिस्सा थोड़ा घमंडी नहीं सुनाई देता क्या?
    'static lifetime और Send/Sync constraints की समस्याएँ developers को व्यापक रूप से महसूस होती हैं, और मुझे ऐसा impression नहीं मिला कि वे बेवकूफ़ हैं

    • यह पहले कही गई बात की ओर फिर इशारा कर रहा है
      लोग कहते हैं कि work stealing न करना आसान और तेज़ है
      मेरा तर्क है कि यह दोनों में से एक है
      non-shared तरीका तेज़ होना है तो code को ऐसे design करना पड़ेगा जो shared-state architecture को thread-safe बनाने से आसान नहीं है
      अगले paragraph में “slow” के जवाब में parallel sentence है
      मैं यह नहीं सोचता कि parallel/concurrent Rust को compile कराने में कठिनाई झेलने वाले लोग बेवकूफ़ हैं
      बस मुझे यह पसंद नहीं कि वे ऐसे behave करें जैसे हमने उनके लिए बनाई API ने उनकी ज़िंदगी खराब कर दी हो
    • “मुश्किल” quotes में है क्योंकि यह ज़रूरी नहीं कि सच में ज़्यादा मुश्किल हो
      अगर वह काम वैसे भी करना है, तो वह ज़्यादा मुश्किल नहीं है
      बात “लोग छोटी चीज़ पर overreact करते हैं” से ज़्यादा यह है कि async उस समस्या को पहले हल करवाता है जिसे कभी न कभी हल करना ही था
      borrow checker जैसा ही अहसास है
      कभी-कभी यह ज़रूरत से ज़्यादा restrictive होता है, लेकिन कभी-कभी जब आप मान लेते हैं कि सब ठीक है, तब कोई corner case सच में मौजूद होता है जिस पर आपने विचार नहीं किया था
    • व्यक्तिगत रूप से मुझे भी कभी-कभी यह समझने में सच में कठिनाई होती है कि मेरी state 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] क्यों लगा रहे हैं, यह समझ नहीं आता

    • एक वजह यह हो सकती है कि अगर thread-per-core performance पर्याप्त है, तो शायद आप शुरुआत से ही Rust इस्तेमाल नहीं करना चाहेंगे
      ऐसी भाषाएँ हैं जिनका 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 होता है” जैसा?

    • मान लें आप लगभग ऐसा code लिखते हैं।
      एक Server है, और serve में message पढ़ने के बाद हर message handler को नई task के रूप में spawn(async move { ... }) किया जाता है।
      शुरुआत में सब ठीक चलता है।
      फिर एक दिन आप do_this का implementation बदलते हैं, जिससे this का type अब Send नहीं रह जाता, और spawn(...) पर आपको एक डरावना compile error मिलता है कि anonymous async move { } scope ने जो type बनाया है वह Send नहीं है।
      वजह हमेशा साफ नहीं होती, और error message भी मददगार नहीं होता।
      अगर this Send नहीं है, तो उसे 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 Send implement करता है या नहीं, यह हमेशा स्पष्ट नहीं होता, इसलिए यह code में सूक्ष्म रूप से घुस जाता है और बाद में अस्पष्ट तरीके से टूट सकता है।
    • कुल मिलाकर compiler पता लगा लेता है कि कुछ 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 करना होता है।
    • मुझे लगता है कि thread-per-core को default बनाने के खिलाफ तर्क को सरल बनाया जा सकता है।
      अगर workload CPU-centric है, तो work stealing ज्यादातर मामलों में बेहतर होगा।
      अगर I/O-centric है, तो thread-per-core बेहतर काम कर सकता है, लेकिन वैसे भी CPU headroom काफी होता है, इसलिए performance बहुत मायने नहीं रखती।
      मेरे हिसाब से work stealing, language API में रखने के लिए बेहतर default है।