Practical multithreading best practices: .NET संस्करण — और thread जोड़ने से पहले क्या तय करें

· अद्यतन तिथि: · · Windows, multithreading, C#, .NET, business app, bug investigation, design

संशोधन इतिहास (पहला संस्करण, 2 Aug 2026 को प्रकाशित)
पहला प्रकाशन
इस लेख का हवाला दें(DOI: 10.5281/zenodo.22175826)

यह लेख Zenodo पर संग्रहीत है। नीचे वह DOI है जो हमेशा नवीनतम संस्करण की ओर ले जाता है, और वह DOI भी जो आपके पढ़े जा रहे संस्करण से बँधा है।

Go Komura (2026). Practical multithreading best practices: .NET संस्करण — और thread जोड़ने से पहले क्या तय करें. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22175826 https://comcomponent.com/hi/blog/multithreading-best-practices-dotnet/

DOI (नवीनतम संस्करण)
10.5281/zenodo.22175826
DOI (यह संस्करण)
10.5281/zenodo.22175827

«Processing धीमा था, इसलिए thread जोड़कर parallel किया, अब sum कभी-कभी गलत आता है।» «Background work जोड़ा, अब महीने में एक बार app जम जाता है।» «Debugger में reproduce नहीं होता कहते हैं, पर ग्राहक स्थल पर निश्चित होता है।» — Multithreaded programming का ख़तरा यह है कि लिखते ही सही चलता दिखता है। Race-condition bugs time-dependent होते हैं: testing से निकल जाते हैं और केवल production में चेहरा दिखाते हैं।

साथ ही, multicore hardware अब सामान्य है, इसलिए business app में भी multithreading टाली नहीं जा सकती — «UI जमाए बिना भारी काम चलाएँ», «कई devices या फ़ाइलें concurrently process करें» जैसी requirements। महत्त्वपूर्ण है और thread जोड़ने से पहले design सिद्धांत तय करना। Multithreading bugs debugging से नहीं मिटते; design से उनके घुसने की जगह ही मिटाते हैं।

यह लेख practical multithreading श्रृंखला का .NET संस्करण है। Windows पर business app बनाते हुए जिन्हें multithreading जोड़नी पड़े, उनके लिए भाषा और OS से निरपेक्ष design सिद्धांत और C#/.NET के ठोस tools, अगस्त 2026 तक के primary sources पर आधारित। सिद्धांत स्वयं Linux या C++ पर नहीं बदलते। Native code लिख रहे हों तो वही सिद्धांत प्रत्येक भाषा के tools पर map करने वाले «C++ संस्करण» और «C संस्करण» देखें, Java लिख रहे हों तो «Java संस्करण»।

1. पहले निष्कर्ष

  • पहली best practice यह है कि thread स्वयं न बनाएँ। new Thread के बजाय Task, thread pool, Parallel class जैसे ऊपरी API पर चलें, और thread count का management runtime पर छोड़ दें।12
  • Parallel करते समय सबसे पहले काटें «shared mutable state»। कई threads एक ही variable में लिखें, वही race का स्रोत है; lock से सुरक्षित करने से पहले partitioning, immutability, और hand-off से sharing स्वयं घटाएँ।3
  • Lock discipline रखें। «कौन सा lock कौन सा data सुरक्षित करता है» one-to-one तय करें, और lock object बाहर से न दिखने वाला dedicated instance हो। lock(this) और lock(typeof(X)) वर्जित हैं। .NET 9 से dedicated System.Threading.Lock type इस्तेमाल करें।4
  • Threads के बीच data hand-off queue पर केंद्रित करें। System.Threading.Channels या concurrent collections पर बना producer/consumer रूप इधर-उधर lock बिखेरने से सरल design देता है, और सीमा भी साफ़ रहती है।56
  • रुकने का तरीका सबसे पहले design करें। Stop के लिए CancellationToken से cooperative cancellation एकमात्र सही उत्तर है; Thread.Abort .NET (Core वंश) पर runtime exception फेंकता है।78
  • UI केवल UI thread का है। WinForms controls और WPF elements, जिस thread ने उन्हें बनाया उसके अलावा किसी से न छुएँ। दूसरे thread से Control.Invoke / Dispatcher के ज़रिए request करें।910
  • «Parallel मतलब तेज़» हमेशा सत्य नहीं। Per-iteration काम छोटा हो तो parallelism का overhead उलटा धीमा कर सकता है। अपनाने से पहले हमेशा मापें।3

Diagram में solid line हमेशा लागू रहने वाला relation दिखाती है और dashed line conditional relation दिखाती है (शर्तें detail page पर हर relation के explanation में दी गई हैं)। Relations की पूरी list (कुल 28, evidence और certainty सहित) तथा मुख्य concepts की definitions knowledge map की detail page पर संकलित हैं (जापानी में)। Data: JSON-LD / Turtle

2. Multithreading कठिन क्यों है — race condition और deadlock

उबालें तो multithreading दो तरह की समस्याएँ लाती है।4

Race condition वह bug है जिसमें परिणाम इस पर depend करता है कि कई threads किसी खास code तक किस क्रम में पहुँचते हैं। Classic उदाहरण shared counter की increment है: एक पंक्ति count++ वास्तव में तीन चरणों में टूटती है — «पढ़ो → जोड़ो → वापस लिखो»। दो threads इन तीन चरणों को एक साथ चलाएँ तो एक thread का जोड़ दूसरे की write-back से overwrite होकर खो जाता है। परिणाम हर run पर बदलता है, और कौन-सा परिणाम मिलेगा unpredictable है।4

Shared counter की race conditionShared counter पर जोड़ खो जाने वाली classic race condition। count++ के तीन चरणों के दौरान दूसरा thread बीच में आए तो जो अंतिम वापस लिखता है वही दूसरे को overwrite करता हैThread BShared variable countThread AThread BShared variable countThread Acount = 10दो बार जोड़ने पर भी count = 11Thread A का जोड़ खो गयापढ़ें (10)पढ़ें (10)Local जोड़ (11)Local जोड़ (11)वापस लिखें (11)वापस लिखें (11)

चित्र 1: Shared counter पर जोड़ खो जाने वाली classic race condition। count++ के तीन चरणों के दौरान दूसरा thread बीच में आए तो जो अंतिम वापस लिखता है वही दूसरे को overwrite करता है

Deadlock वह state है जिसमें दो threads प्रत्येक उस lock की wait करते हैं जो दूसरा पकड़े है, इसलिए कोई आगे नहीं बढ़ सकता। Thread A lock 1 पकड़े lock 2 की wait करता है; Thread B lock 2 पकड़े lock 1 की wait करता है — इतना ही दोनों को सदा के लिए रोकने को काफी है।4

Deadlock की circular waitDeadlock की circular wait। Wait के तीर जैसे ही loop बनाते हैं, उस loop का हर thread सदा रुक जाता हैLock 2 की release की waitLock 1 की release की waitThread ALock 1 पकड़ेThread BLock 2 पकड़े

चित्र 2: Deadlock की circular wait। Wait के तीर जैसे ही loop बनाते हैं, उस loop का हर thread सदा रुक जाता है

दोनों को असुविधाजनक बनाने वाली बात time-dependence है। Development मशीन पर दसियों हज़ार runs में एक बार लगने वाला interleaving (execution order का combination) ग्राहक की मशीन पर, अलग core count और अलग timing के साथ, हर दिन हो सकता है। «Debugger लगे होने पर reproduce नहीं होता» और «log जोड़ते ही गायब हो गया» दोनों इसलिए होते हैं क्योंकि स्वयं observation timing बदल देता है — यही race-bug का classic व्यवहार है।

इसीलिए आगे के हर सिद्धांत की दिशा एक है: «सही synchronize करने» से पहले «synchronization चाहिए वाले स्थान घटाएँ» — यही multithread design का मूल सिद्धांत है।

3. सिद्धांत 1: Thread स्वयं न बनाएँ

3.1. Task और thread pool पर चलें

new Thread(...) से thread सीधे बनाना आज के .NET में असाधारण अंतिम उपाय है। .NET Framework 4 से multithreaded और parallel code का recommended साधन TPL (Task Parallel Library) है — अर्थात् Task केंद्रित API परिवार। TPL उपलब्ध processors के अनुसार parallelism dynamically adjust करता है, और काम बाँटना, thread pool पर schedule करना, cancellation सँभालना, state management — ये low-level झंझट सब अपने सिर लेता है।1

Thread pool वह आधार है जिसे .NET स्वयं व्यापक रूप से इस्तेमाल करता है — Task चलाने, async I/O complete करने, timer callbacks आदि के लिए — और छोटे काम फेंकते रहें तो developer को thread का lifecycle स्वयं सँभालने की ज़रूरत नहीं।2

// CPU भरने वाली भारी computation background में
var result = await Task.Run(() => HeavyCalculation(input));

// कई स्वतंत्र कार्य concurrently चलाकर सबकी wait (संख्या कम हो)
// ※ यह रूप तब जब ProcessAsync I/O-bound async method हो।
//   WhenAll केवल «पहले से चल रहे Task की wait» करता है, इसलिए CPU computation
//   concurrently चलानी हो तो प्रत्येक को Task.Run(() => Calc(x)) से लपेट thread pool पर डालें
var results = await Task.WhenAll(items.Select(x => ProcessAsync(x)));

// संख्या अधिक हो तो concurrency पर ऊपरी सीमा लगाकर बहाएँ
await Parallel.ForEachAsync(items,
    new ParallelOptions { MaxDegreeOfParallelism = 8, CancellationToken = callerCt },
    async (x, ct) => await ProcessAsync(x, ct));
// दो बातें: caller का token ParallelOptions से जोड़ें
// (भूलें तो शरीर का ct हमेशा None रहता है), और वही ct शरीर में भी दें (फेंकें नहीं)

एक सावधानी। Task.WhenAll(items.Select(...)) जिस क्षण enumeration होता है हर element का processing एक साथ शुरू करता है। मुट्ठी भर से कुछ दर्जन निश्चित कामों पर ठीक है, पर बड़े collection पर इस्तेमाल करें तो sockets, DB connections और memory एक साथ खत्म हो जाते हैं। जिस काम की मात्रा पहले से न पता हो, ऊपर जैसे Parallel.ForEachAsync से concurrency सीमित करें, या आगे वर्णित bounded channel से flow control करें।

अपना thread लगभग तभी जायज़ है जब thread का गुण स्वयं requirement हो — «dedicated message loop चाहिए», «threading apartment (STA) निर्दिष्ट करना हो», या «app के पूरे जीवन भर चलना हो»।

3.2. Data parallelism के लिए Parallel.For / ForEach

Data parallelism — «collection के हर element पर वही processing लगाकर पूरा काम तेज़ करना» — के लिए loop स्वयं threads में न बाँटें; Parallel.For / Parallel.ForEach इस्तेमाल करें। Data source बाँटना (partitioning) और load rebalancing TPL करता है, और मूल loop में lock भी नहीं चाहिए।11

पर official documents दो pitfalls स्पष्ट लिखते हैं।3

  • यह न मानें कि parallel हमेशा तेज़ है। कम iterations वाला, या per-iteration हल्का काम वाला loop parallelism के overhead से उलटा धीमा हो सकता है। Performance कई factors पर depend करता है, इसलिए हमेशा मापकर तय करें।
  • Iterations को एक-दूसरे की wait न कराएँ। Parallel.For की प्रत्येक iteration वास्तव में parallel चले, इसकी guarantee नहीं। एक iteration दूसरी के set event की wait करे, तो scheduling के अनुसार deadlock हो सकता है।

3.3. «Wait» वाला काम thread नहीं, async I/O को

फ़ाइल, network, database जैसे मुख्यतः I/O की wait वाला काम thread जोड़ने का विषय नहीं है। Wait करते पूरा thread बाँधना व्यर्थ है; async/await से async I/O wait के दौरान कोई thread नहीं खाता। यह भेद — CPU-bound काम parallel करें, I/O-bound काम async बनाएँ — multithread design के द्वार पर खींची जाने वाली पहली रेखा है।

Thread खड़ा करने से पहले की शाखाएँ«Thread खड़ा करने» से पहले की शाखाएँ। अधिकांश business processing ऊपर के तीन निकासों में से किसी में गिरता है, और new Thread तक पहुँचना असाधारण मामला ही हैI/O wait प्रधानफ़ाइल, network, DBCPU वाली computationCollection के हर element परवही processingस्वतंत्र एक टुकड़ाbackground processingMessage loop या STA आदिthread का गुण ही requirementConcurrently चलाना है कामकाम का प्रधान भाग?async/await async I/Othread न जोड़ेंकाम का रूप?Parallel.For / ForEachTask.Run / Task.WhenAllnew Thread(असाधारण अंतिम उपाय)

चित्र 3: «Thread खड़ा करने» से पहले की शाखाएँ। अधिकांश business processing ऊपर के तीन निकासों में से किसी में गिरता है, और new Thread तक पहुँचना असाधारण मामला ही है

async/await के practical निर्णय «C# async/await practical decision table — Task.Run और ConfigureAwait» में हैं, और उसके नीचे thread pool और async I/O कैसे जुड़ते हैं «Windows I/O की गहराई (भाग 3) — I/O Completion Ports (IOCP) और .NET Thread Pool» में विस्तार से हैं।

4. सिद्धांत 2: Shared mutable state न्यूनतम करें

Race तभी उठती है जब «कई threads» और «shared mutable data» दोनों हों। Threads की संख्या requirements तय करती हैं, इसलिए design काट सकता है sharing। साधन तीन हैं।

4.1. Partition करें — प्रत्येक thread केवल अपना data छुए

सबसे सरल और शक्तिशाली तरीका data को thread के अनुसार बाँट देना है। Parallel loop के aggregation में हर iteration shared sum variable में लिखने के बजाय, thread-local state लेने वाले Parallel.For overload से प्रत्येक thread अपना local sum बनाए, और अंत में एक बार मिलाएँ। Shared state पर write «हर iteration» से «प्रति thread एक बार» तक गिरता है, synchronization लागत और race की खिड़की दोनों परिमाणों में सिकुड़ते हैं।3

long total = 0;
Parallel.For(0, items.Length,
    () => 0L,                                     // Thread-local initial value
    (i, state, local) => local + Weigh(items[i]),  // प्रत्येक iteration केवल अपने local में जोड़ती
    local => Interlocked.Add(ref total, local));   // Merge प्रति thread एक बार
Thread-local aggregationThread-local aggregation। Processing के दौरान प्रत्येक thread केवल अपना data छूता है, इसलिए race की जगह नहीं; shared state पर write merge के समय प्रति thread एक बार ही होता हैData array (processing target)Thread 1अपना हिस्सा process करकेवल local sum में जोड़ताThread 2अपना हिस्सा process करकेवल local sum में जोड़ताThread 3अपना हिस्सा process करकेवल local sum में जोड़ताMerge: Interlocked.Add सेप्रति thread एक बार sum में जोड़ें

चित्र 4: Thread-local aggregation। Processing के दौरान प्रत्येक thread केवल अपना data छूता है, इसलिए race की जगह नहीं; shared state पर write merge के समय प्रति thread एक बार ही होता है

4.2. Immutable बनाएँ — जो नहीं लिखते, स्वतंत्रता से share करें

केवल पढ़े जाने वाला data कितने भी threads से एक साथ पढ़ना सुरक्षित है। Config values, master data, computation input आदि निर्माण के बाद न लिखे जाएँ (immutable बनें) तो बिना synchronization स्वतंत्रता से share हो सकते हैं। C# में record types और init properties इस design को सहारा देते हैं। केवल यह तय करना कि «बदलाव चाहिए तो मौजूदा को लिखने के बजाय नया instance बनाकर बदलें» एक और mutable state हटाता है जिसे अन्यथा सुरक्षित रखना पड़ता।

पर «read-only दिखता है» और «immutable है» अलग बातें हैं। IReadOnlyList<T> जैसा read-only interface केवल यही कहता है कि «उस interface से नहीं लिख सकते» — पीछे की List<T> को दूसरे reference से लिखना नहीं रोकता। record / init की guarantee भी shallow है: property जिस object की ओर इशारा करता है उसे नहीं बचाती। Threads के बीच वास्तव में सुरक्षित share करना हो तो ImmutableArray<T> जैसे System.Collections.Immutable का immutable collection इस्तेमाल करें, या share करते क्षण copy दें और लिखने का मार्ग ही काट दें। तब शर्त यह है कि element type T स्वयं भी immutable हो। Immutable collection केवल «क्रम» सुरक्षित करता है; mutable element objects के references ज्यों के त्यों share रहते हैं, इसलिए किसी और मार्ग से element की सामग्री लिखी जा सके तो race रहती है। Object graph पत्तियों तक immutable बनाएँ, या deep copy दें।

4.3. Hand-off करें — share करने के बजाय queue से भेजें

फिर भी threads के बीच data चलाना पड़ता है। तब «shared variable दोनों ओर से छूना» नहीं, एक पक्ष लिखे, दूसरा पढ़े, बीच में queue वाला producer/consumer रूप अपनाएँ।

.NET पर पहला उम्मीदवार System.Threading.Channels है। Producer async लिखता है, consumer async पढ़ता है — FIFO — और synchronization की झंझट सब channel सँभालता है।5

var channel = Channel.CreateBounded<WorkItem>(100); // क्षमता 100 — backpressure लगाता है

// Producer पक्ष
await channel.Writer.WriteAsync(item, ct); // भरा हो तो जगह खुलने तक wait
// …सभी producers लिख चुके:
channel.Writer.Complete();   // «अब और नहीं» घोषित करता है। बिना इसके reader का loop खत्म नहीं हो सकता

// Consumer पक्ष
await foreach (var item in channel.Reader.ReadAllAsync(ct))
{
    Process(item);
}
Channel वाला producer/consumer रूपChannel के इर्द-गिर्द producer/consumer रूप। कोई पक्ष shared variable सीधे नहीं छूता; wait और capacity control दोनों channel पर छोड़ दिए जाते हैंभरा हो तो write wait(backpressure)खाली हो तो read waitProducer 1WriteAsyncbounded channel (क्षमता 100)FIFO queuesynchronization channel सँभालताProducer 2WriteAsyncConsumer 1ReadAllAsyncConsumer 2ReadAllAsync

चित्र 5: Channel के इर्द-गिर्द producer/consumer रूप। कोई पक्ष shared variable सीधे नहीं छूता; wait और capacity control दोनों channel पर छोड़ दिए जाते हैं

व्यवहार में महत्त्वपूर्ण है capacity-limited (bounded) channel चुनना। Limit लगते default व्यवहार «writer जगह की wait करे» है, और वही प्राकृतिक backpressure बनता है। Production consumption से तेज़ हो और unbounded queue इस्तेमाल करें तो time bomb बनता है: चलता रहता है, पर memory बढ़ती जाती है।5

Synchronous दुनिया में bounded channel की भूमिका क्षमता बताए BlockingCollection<T> निभाता है। Capacity limit producer को consumer से बहुत आगे निकलने से रोकती है, और खाली होने पर consumer को block कर wait कराती है — blocking और capacity control दोनों।12 दूसरी ओर ConcurrentQueue<T> / ConcurrentStack<T> lock के बिना केवल Interlocked operations से thread-safety पाने वाले तेज़ collections हैं6, पर न capacity limit है न «खाली हो तो wait» तंत्र — सादे thread-safe queues। इन्हें hand-off के design का नायक नहीं, component समझें। और BlockingCollection<T> async access के लिए design नहीं, इसलिए async/await के साथ जोड़ें तो Channel<T> चुनें।12

एक और सावधानी: «Dictionary ConcurrentDictionary से बदल दिया, इसलिए thread-safe» वाली धारणा से बचें। अलग-अलग operations thread-safe हों, तब भी «मौजूद है या नहीं जाँचो, फिर जोड़ो» जैसी combined operations अभी भी race करती हैं (GetOrAdd जैसे combined operation के लिए बने method इस्तेमाल करें)। और GetOrAdd का अपना pitfall है: अंत में stored value एक ही रहने की guarantee है, पर value बनाने वाला factory function contention पर एक से अधिक बार बुलाया जा सकता है। Factory में side effect डालें — connection खोलना, फ़ाइल बनाना आदि — तो double execution से leak होता है, इसलिए factory side-effect-free रखें, या ठीक एक बार चाहिए initialization के लिए value के रूप में Lazy<T> store करें। Collection का type बदलना shared mutable state घटाने का विकल्प नहीं।

5. सिद्धांत 3: Lock discipline रखें

Shared mutable state घटाने के बाद भी अक्सर शून्य तक नहीं पहुँचते। जो shared बचा उसके लिए mutual exclusion (lock) इस्तेमाल करें, पर lock «जो जगह संदिग्ध लगे उसे lock से घेर दो» का औज़ार नहीं। Discipline चार बिंदु हैं।

5.1. «क्या सुरक्षित कर रहे हैं» तय करें, dedicated object से lock करें

Lock की इकाई «code का टुकड़ा» नहीं «data» सोचें। सुरक्षित करने वाले हर mutable data समूह को एक lock object दें, और उस data को छूने वाले हर स्थान पर वही lock लें — इस तालिका का टूटा रूप ही race bug वास्तव में है।

Lock object बाहर न उजागर dedicated instance हो। lock(this) आपके instance का reference रखने वाले बाहरी code से lock share करता है, lock(typeof(X)) पूरे application domain से — दोनों deadlock के अड्डे हैं। .NET 9 / C# 13 से dedicated type System.Threading.Lock का instance lock object बनाना recommended है।4

public class OrderBook
{
    private readonly Lock _gate = new();          // .NET 9+ (उससे पहले readonly object)
    private readonly List<Order> _orders = [];    // _gate जो data सुरक्षित करता है

    public void Add(Order order)
    {
        lock (_gate) { _orders.Add(order); }
    }
}

C# का lock statement exception फेंके जाने पर भी lock विश्वसनीय रूप से छोड़ने की guarantee देता है। Expansion lock object के type पर depend करता है: साधारण object पर finally में Monitor.Exit का call बनता है, Lock type पर EnterScope() और उसका dispose।413 अर्थात् Lock type का field Monitor से अलग तंत्र है, और यदि code का कोई भाग हाथ से Monitor.Enter(_gate) लिखे तो lock (_gate) के साथ mutual exclusion नहीं ठहरता। दोनों types पर Monitor.Enter / Exit हाथ से लिखना बंद करें और पूरी तरह lock syntax पर एक करें — यही सुरक्षित है।4

5.2. Lock पकड़े धीमा या बाहरी काम न करें

Lock जितनी देर कम पकड़ें उतना अच्छा; पकड़े केवल वही करें जो वह data पढ़ना-लिखना है। Lock पकड़े I/O करना, या event या callback से बाहरी code बुलाना, पकड़ केवल लंबी नहीं करता — वह मार्ग खोलता है जहाँ बुलाया गया code दूसरा lock लेने की कोशिश कर deadlock करे। Lock के बाहर तैयार करें, lock के भीतर केवल बदलें — यही मूल रूप है।

ध्यान दें कि lock के भीतर await नहीं कर सकते (compile error)। यह restriction नहीं, रक्षा है: Monitor में thread affinity है — जिस thread ने lock लिया वही छोड़ना चाहिए — जो async code से मेल नहीं खाती, जहाँ await के आर-पार executing thread बदल सकता है। Async code में mutual exclusion के लिए प्रारंभिक count 1 वाला SemaphoreSlim इस्तेमाल करें।14

private readonly SemaphoreSlim _asyncGate = new(1, 1);

public async Task SaveAsync(Data data, CancellationToken ct)
{
    await _asyncGate.WaitAsync(ct);
    try   { await WriteToFileAsync(data, ct); }
    finally { _asyncGate.Release(); }
}

5.3. कई locks हमेशा एक ही क्रम में लें

दो या अधिक locks हों तो लेने का क्रम thread के अनुसार पलटना deadlock का classic pattern है। उपाय सरल है: नियम बनाएँ कि हर thread lock एक ही क्रम में ले। क्रम guarantee न हो तो Monitor.TryEnter का timeout overload इस्तेमाल करें; न मिल सके तो छोड़कर फिर कोशिश करें (या anomaly log करें) — शाश्वत hang पता लगाने योग्य failure बन जाता है।4

5.4. सरल update Interlocked, read प्रधान हो तो ReaderWriterLockSlim

एक variable का atomic update — counter बढ़ाना-घटाना, flag बदलना — lock से तेज़ Interlocked class (Increment / Add / CompareExchange) है। Contention न हो तो एक CPU instruction prefix जितना सस्ता पड़ सकता है।4 उलटा, Interlocked वहीं तक है; कई variables एक साथ consistent नहीं रख सकता। volatile के साथ हाथ का lock-free ढाँचा memory model की गहरी समझ माँगने वाला specialist औज़ार है, business app में लिखने की चीज़ नहीं।

«Read बार-बार, write दुर्लभ» shared data पर ReaderWriterLockSlim विकल्प भी है, जो केवल write exclude करता है और read concurrently चलने देता है।13

6. सिद्धांत 4: रुकने का तरीका सबसे पहले design करें

Multithread design review का पहला प्रश्न «यह कैसे रुकता है» होना चाहिए। चलने वाला code बिना सोचे लिखा जा सकता है; सुरक्षित रुकने वाला code design किए बिना जन्म नहीं लेता।

6.1. Cooperative cancellation (CancellationToken) एकमात्र सही उत्तर

.NET का रुकने का मॉडल cooperative cancellation पर एक है। रोकने वाला पक्ष CancellationTokenSource बनाता है और उसका Token प्रत्येक processing को देता है। रोकना हो तो Cancel() बुलाता है। Processing पक्ष token देखता है और अपनी सुविधा के बिंदु पर साफ़ कर खत्म होता है — ज़बरदस्ती नहीं सहयोग, इसलिए processing पक्ष state consistent रखते हुए खत्म हो सकता है।7

private CancellationTokenSource? _cts;
private Task? _worker;

public void Start()
{
    if (_worker is { IsCompleted: false })    // चलते दोहरा Start अस्वीकार करें
        throw new InvalidOperationException("Worker पहले से चल रहा है।");
    if (_worker is { IsFaulted: true })       // पिछली failure निगलकर ऊपर से न बनाएँ
        throw new InvalidOperationException("पिछला worker fail हो चुका है।", _worker.Exception);
    _cts = new CancellationTokenSource();
    var token = _cts.Token;   // Stop के बाद पुनः-Start से race न हो, पहले local में पकड़ें
    _worker = Task.Run(() => WorkLoop(token), token);
}

private void WorkLoop(CancellationToken ct)
{
    while (!ct.IsCancellationRequested)   // Polling से निगरानी
    {
        ProcessNextItem(ct);              // Block कर सकने वाली call को ct दें, तुरंत interrupt हो
    }
}

public async Task StopAsync()
{
    var cts = _cts;          // Wait के दौरान fields बदल जाएँ तो भी
    var worker = _worker;    // गलत लक्ष्य न रोकें — local में स्थिर करें
    if (cts is null || worker is null) return;

    Exception? cancelFailure = null;
    try { cts.Cancel(); }    // Token पर registered callback exception फेंक सकता है
    catch (Exception ex) { cancelFailure = ex; }   // पकड़ें, join पूरा कर फिर report करें

    try
    {
        try { await worker; }    // Cancel सफल हो या न हो, join हमेशा पूरा करें, बीच की failure भी देखें
        catch (OperationCanceledException ex) when (ex.CancellationToken == cts.Token)
        { }                      // केवल वही cancellation «सामान्य» गिनें जो हमने माँगा
        catch (Exception ex) when (cancelFailure is not null)
        {
            throw new AggregateException(cancelFailure, ex);  // दोनों failures न खोएँ
        }
    }
    finally
    {
        cts.Dispose();           // Join के बाद source dispose करें (WaitHandle जैसे OS resources छोड़ता है)।
        if (ReferenceEquals(_cts, cts))
        {
            _cts = null;         // Disposed source बाद के StopAsync को न दें
            _worker = null;
        }
    }
    if (cancelFailure is not null)
        throw new AggregateException(cancelFailure);
}

ध्यान दें कि यह Start / StopAsync एक ही thread (उदाहरण UI thread) से क्रम में बुलाए जाने की न्यूनतम रचना है। कई threads lifecycle एक साथ चला सकते हों तो Start / StopAsync स्वयं SemaphoreSlim आदि से serialize करें — worker सुरक्षित करने से पहले management operations स्वयं race करें तो उद्देश्य ही उलटा हो जाता है।

इस छोटे sample में भी व्यवहार में काम आने वाली तरकीबें हैं। पहला, Start चलते दोहरी call अस्वीकार करता है। _cts और _worker बिना शर्त overwrite करें तो पिछले worker का reference खो जाता है, और न रोकने न join करने योग्य «भटका thread» साथ चलता रहता है। Lifecycle API (Start/Stop) पर «एक समय एक» स्वयं लागू करना मूल है। उसके अलावा तीन बातें। पहली, Stop API completion की wait करता है। Cancel() केवल cancellation «माँगता» है; लौटते क्षण worker अभी ProcessNextItem के बीच हो सकता है। केवल माँगकर लौटने वाला Stop() नया race बनाता है: caller साफ़ करना शुरू करे और worker अभी चल रहा हो। दूसरी, Task फेंकें नहीं, पकड़ें। _ = Task.Run(...) से फेंक दें तो worker exception से मरे, कोई न जाने। तीसरी, token _cts.Token lambda के भीतर reference न करें — local variable में पकड़कर दें। Lambda के भीतर referenced हो तो evaluation execution time पर होता है, और stop के तुरंत बाद पुनः-Start हो तो पुराना worker नया token पकड़ लेता है। वही token Task.Run के दूसरे argument में भी दें तो processing पक्ष ThrowIfCancellationRequested या cancellation-aware API के OperationCanceledException से खत्म होने पर Task «failed (Faulted)» नहीं «cancelled (Cancelled)» classified होता है (इस उदाहरण की तरह loop शर्त से सामान्य निकास successful completion गिना जाता है)। एक और: StopAsync का catch when filter से केवल अपने token से जन्मा cancellation पकड़ता है। OperationCanceledException बिना शर्त निगलें तो processing के भीतर दूसरे tokens — per-item timeout आदि — की असली failure भी «रुक गया, इसलिए ठीक» दिखेगी। ध्यान दें कि token-match से यह पहचान WorkLoop के भीतर linked token (6.1 का link combination) इस्तेमाल करने पर टूटती है, क्योंकि उड़कर आने वाले exception पर link-पक्ष का token होता है। उस रचना में, WorkLoop के निकास पर ct.ThrowIfCancellationRequested() बुलाकर बाहरी token में «translate» कर निकलें, या filter when (cts.IsCancellationRequested) तक ढीला कर «stop माँगे रहते cancellation सामान्य» मान लें — दोनों में से एक design के रूप में स्पष्ट चुनें।

Cooperative cancellation का रूपCooperative cancellation का रूप। रोकने वाला पक्ष केवल Cancel() बुलाता है; «कब और कैसे खत्म हो» प्रत्येक processing स्वयं तय करता है। इसलिए state consistent रखते हुए रुक सकता हैCancel() एक बार बुलाताToken सौंपताToken सौंपताToken सौंपताIsCancellationRequested जाँचसाफ़ कर स्वयं खत्म होताThrowIfCancellationRequestedWait के बीच भी तुरंत interruptरोकने वाला पक्षCancellationTokenSourceWorker processing 1Worker processing 2Library काcancellation-aware APINormal completionOperationCanceledException= cancellation पूर्ण माना जाताCancellation पूर्ण

चित्र 6: Cooperative cancellation का रूप। रोकने वाला पक्ष केवल Cancel() बुलाता है; «कब और कैसे खत्म हो» प्रत्येक processing स्वयं तय करता है। इसलिए state consistent रखते हुए रुक सकता है

Library पक्ष की रीति भी तय है। Cancel हो सकने वाला operation CancellationToken लेने वाली public method दे, और computation loop में समय-समय पर IsCancellationRequested जाँचे या ThrowIfCancellationRequested() बुलाए। दूसरा OperationCanceledException फेंकता है, जिसे Task «failure» नहीं «cancellation पूर्ण» मानता है। बाहरी token और आंतरिक कारण (timeout आदि) दोनों से रोकना हो तो linked tokens से combine करें।7

6.2. Thread.Abort को अस्तित्वहीन मानें

«न सुनने वाले thread को बाहर से मारना» — Thread.Abort — .NET Core / .NET 5 और बाद में केवल PlatformNotSupportedException फेंकता है; अब इस्तेमाल ही नहीं हो सकता। Thread कहाँ चल रहा है जाने बिना उसमें exception फेंकना resource cleanup को बाधित कर सकता है और state corrupt कर सकता है। Cooperative cancellation का जवाब न देने वाले (या देने लायक न लिखे जा सकने वाले) third-party code को ज़बरदस्ती खत्म करना हो तो official guidance है: अलग process में चलाएँ और Process.Kill से रोकें।8

6.3. Wait polling से नहीं, wait handle से

«Flag उठे तक Sleep(100) loop में wait» लिखना CPU और responsiveness दोनों बर्बाद करता है। Threads के बीच संकेत के लिए ManualResetEventSlim और SemaphoreSlim जैसे synchronization primitives हैं, जो संकेत मिलने तक thread को सही सुलाते हैं।13 Windows पर timer precision और event wait का चुनाव «Windows पर Sleep(1) के बजाय Event Wait क्यों चुनें» में विस्तार से है।

7. UI thread की विशेष स्थिति — Windows desktop app का नियम

Windows desktop app पर सामान्य सिद्धांतों के ऊपर एक और मज़बूत बाधा है। नियम: UI केवल उसी thread से छूआ जा सकता है जिसने उसे बनाया (UI thread)।

WinForms controls thread-safe नहीं; कई threads से चलाने पर control inconsistent state में धकेल जाता है — race, deadlock, freeze का कारण। Windows app से माँगता है कि system messages लेने वाला एक dedicated thread हो, और UI का निर्माण तथा संचालन उसी thread पर केंद्रित हो।9 WPF की संरचना बिल्कुल वही है: UI element केवल UI thread बदल सकता है।10

दूसरे thread से UI update करना हो तो सीधे न छुएँ — «UI thread को request» में बदलें।

UI update को request में बदलनाUI update को «request» में बदलें। Background thread का काम message queue पर काम रखवाने तक है — control हमेशा UI thread स्वयं छूता हैControl.Invoke /Dispatcher.InvokeAsync से requestControl सीधे छूनाWindowsmouse, keyboard, repaintUI thread कीmessage queueBackground thread(भारी काम, संचार)UI threadcontrol छूने वाला एकमात्र threadनिषिद्धrace, deadlock, freeze का कारण

चित्र 7: UI update को «request» में बदलें। Background thread का काम message queue पर काम रखवाने तक है — control हमेशा UI thread स्वयं छूता है

Framework Request का साधन
WinForms Control.Invoke (synchronous) / Control.BeginInvoke (async) / .NET 9 से Control.InvokeAsync9
WPF Dispatcher.Invoke (synchronous) / Dispatcher.InvokeAsync / Dispatcher.BeginInvoke (async)10

इनमें synchronous रूप (Control.Invoke / Dispatcher.Invoke) सावधानी माँगते हैं। UI thread उस worker के खत्म होने की synchronous wait कर रहा हो और worker Invoke बुलाए, तो प्रत्येक दूसरे की wait वाला deadlock बनता है (अध्याय 2 की circular wait ठीक वैसी)। Background से सूचना और progress report के लिए async रूप (BeginInvoke / InvokeAsync) default बनाएँ, और synchronous रूप उन्हीं स्थितियों तक सीमित रखें जहाँ निश्चय हो कि UI thread आपकी wait नहीं कर रहा।

व्यवहार में और बेहतर उत्तर है। UI thread पर शुरू processing async/await से लिखें तो await UI thread का SynchronizationContext पकड़ता है और continuation स्वतः UI thread पर फिर चलाता है, इसलिए हाथ से Invoke लिखने की जगहें बहुत घट जाती हैं। पर यह बिना शर्त गुण नहीं। Background callback से घुसा code, या ConfigureAwait(false) के बाद की continuation, UI thread पर नहीं लौटती, इसलिए उस मार्ग पर UI छुएँ तो स्पष्ट dispatch अभी भी चाहिए। इस रूप पर एक करें: «भारी काम Task.Run या async I/O को, परिणाम स्क्रीन पर await के बाद की continuation में» — यही आधुनिक Windows app का मूल रूप है। UI thread और async/await का संबंध «WPF/WinForms async और UI thread एक पन्ने पर» में एक चित्र में समेटा है।

और जहाँ COM शामिल हो — Office integration, legacy components आदि — एक और परत जुड़ती है: COM का अपना threading model (STA/MTA)। «UI thread पर COM object बनाया, दूसरे thread से बुलाया, जम गया» जैसी दुर्घटनाएँ इसी परत की हैं, और «COM STA/MTA मूल बातें — threading model और hang से कैसे बचें» में समझाई गई हैं।

8. Native code (C++/C) में लिखते समय

अब तक के सिद्धांत — thread सीधे न बनाएँ, shared mutable state घटाएँ, lock discipline, रुकने का design — native code पर ज्यों के त्यों लागू होते हैं। बदलते हैं tools। C++ में RAII के साथ std::jthread / std::mutex / std::atomic; C में Win32 API के _beginthreadex, SRW lock, condition variable, और stop-event pattern। प्रत्येक, भाषा-विशिष्ट pitfalls सहित (std::thread का destructor, TerminateThread का ख़तरा, DllMain और loader lock आदि), इस श्रृंखला के «C++ संस्करण» और «C संस्करण» में है।

9. Verification और debug — इस मान्यता पर तैयारी कि reproduce नहीं होगा

Multithreading bugs testing से मिलने की अपेक्षा नहीं कर सकते। सामान्य unit test उस run को success गिनता है जहाँ race संयोग से नहीं लगी। तैयारी तीन परतों में सोचें।

पहली रक्षा रेखा अब तक के design सिद्धांत स्वयं हैं। पाँच shared mutable states वाले app और पचास वाले में संदेह की जगहें दस गुना भिन्न हैं। Review में तालिका से जाँचें: «कौन सा mutable data shared है», «प्रत्येक कौन सा lock सुरक्षित करता है», «lock लेने का क्रम unique है», «stop पथ कहाँ हैं»। वह design जिसके लिए यह तालिका न लिख सकें अधूरा है, चाहे चल रहा हो।

दूसरा, anomalies छिपाने के बजाय observable बनाएँ। Monitor.TryEnter की timeout से lock-wait anomaly पकड़कर log करें,4 thread pool पर फेंके काम के unseen exceptions निगलें नहीं — दर्ज करें, और hang पर full dump लेने को तैयार रहें ताकि हर thread का stack देखा जा सके — «कभी-कभी ही होता» bug से लड़ाई इस बात से तय होती है कि जो एक बार हो, उससे कितनी जानकारी निकाल सकें। Dump और log की व्यवस्था «Windows app crash के लिए logging और dump capture design» में है।

तीसरा, load के नीचे हिलाएँ। Development मशीन पर अशुभ interleaving लगाना आसान बनाने वाला stress-test — cores से अधिक parallelism से लंबे समय चलाना, processing क्रम random करना, कृत्रिम delay घुसाना — shipping से पहले race निकालने का यथार्थवादी साधन है। Debugger में गायब bug अक्सर release build प्लस भारी load के नीचे reproduce होता है।

10. सार — और thread जोड़ने से पहले checklist

उबालें तो multithreaded programming की best practice «synchronization सही लिखने का कौशल» नहीं, «synchronization लिखे बिना चल जाने वाला design» है। शुरू करने से पहले इन आठ प्रश्नों का उत्तर दे सकें तो लगभग हर बड़ी दुर्घटना रुक सकती है।

  1. यह काम CPU-bound है या I/O-bound (बाद वाला हो तो उत्तर thread नहीं, async/await है)?
  2. क्या new Thread लिखने वाले हैं (क्या Task, Parallel, या thread pool से व्यक्त हो सकता है)?
  3. Threads के बीच कौन सा mutable data shared है — गिन सकते हैं?
  4. वह sharing partitioning, immutability, या queue से hand-off से मिटाया जा सकता है?
  5. बचे प्रत्येक shared data के लिए ठीक एक matching lock तय है?
  6. Lock लेने का क्रम हर thread पर unique है, और lock पकड़े बाहरी call तो नहीं?
  7. CancellationToken हर लंबे operation तक पहुँचा है, और stop पथ समझा सकते हैं?
  8. UI छूने वाला code UI thread पर केंद्रित है?

Multithreading bugs लिखे दिन नहीं दिखते — भूल जाने के बहुत बाद ग्राहक स्थल पर दाँत निकालते हैं। उलटा कहें तो design चरण पर यह checklist चला लें तो «कभी-कभी crash», «महीने में एक बार जमना» जैसी सबसे महँगी failures code लिखने से पहले तोड़ी जा सकती हैं।

संबंधित लेख

संबंधित परामर्श क्षेत्र

KomuraSoft LLC multithreading वाले business app की design review, «कभी-कभी crash / जमना» जैसी कठिन-reproduce खराबी की root-cause investigation — dump analysis और race स्थान पिन करना — तथा मौजूदा app को parallel या async बनाने का technical consulting सँभालता है। «यह design race तो नहीं करेगा, जाँचें» जैसे प्रारंभिक चरण से भी बुलाया जा सकता है।

संदर्भ लिंक

  1. Microsoft Learn, Task Parallel Library (TPL). इस पर कि TPL .NET Framework 4 से multithreaded और parallel code का recommended साधन है; parallelism उपलब्ध processors के अनुसार dynamically adjust करता है; काम बाँटना, thread pool पर scheduling, cancellation सँभालना और state management अपने सिर लेता है; per-iteration काम छोटा loop parallelism overhead से धीमा हो सकता है; और TPL इस्तेमाल करने पर भी lock, deadlock और race condition की मूल समझ recommended रहती है। ↩ ↩2

  2. Microsoft Learn, The managed thread pool. इस पर कि ThreadPool class system-managed worker threads का pool देता है, जिससे developer thread management के बजाय app के कार्यों पर केंद्रित रह सकें; और .NET TPL operations, async I/O completion, timer callbacks, registered waits, socket connections आदि के लिए thread pool व्यापक इस्तेमाल करता है। ↩ ↩2

  3. Microsoft Learn, Potential Pitfalls in Data and Task Parallelism. इस पर कि parallel loop कभी sequential से धीमा होता है और हमेशा मापना चाहिए; parallel loop के भीतर shared memory पर write से बचें, thread-local state लेने वाला overload recommended है; और For/ForEach की प्रत्येक iteration वास्तव में parallel चले इसकी guarantee नहीं, इसलिए iterations के बीच wait करने वाला code deadlock कर सकता है। ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, Managed threading best practices. Race condition (counter increment पढ़ने-जोड़ने-वापस लिखने में टूटकर overwrite से खो जाने का उदाहरण) और deadlock की परिभाषाओं पर; Thread.Abort के बजाय cooperative cancellation इस्तेमाल करने पर; type या this को lock object न बनाने, और .NET 9 / C# 13 से dedicated System.Threading.Lock instance इस्तेमाल करने पर; C# lock statement के finally में Monitor.Exit की guarantee पर; Monitor.TryEnter timeout से deadlock पता लगाने पर; सरल state change के लिए Interlocked class तेज़ होने पर; और static data default thread-safe, instance data default non-thread-safe वाली design guidance पर। ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10

  5. Microsoft Learn, System.Threading.Channels library. इस पर कि channel producer/consumer model का FIFO है जो synchronization भीतर सँभालता है; CreateBounded से capacity-limited channel बन सकता है; limit लगते default व्यवहार writer की wait है, DropOldest जैसे अन्य FullMode भी चुन सकते हैं; और write read से तेज़ हो तो backpressure लगता है। ↩ ↩2 ↩3

  6. Microsoft Learn, Thread-safe collections. इस पर कि System.Collections.Concurrent के collections fine-grained lock या lock-free तंत्र से thread-safety पाते हैं; और ConcurrentQueue तथा ConcurrentStack lock के बिना Interlocked operations से लागू हैं, इसलिए कई threads से बार-बार जोड़ने-हटाने पर टिकते हैं। ↩ ↩2

  7. Microsoft Learn, Cancellation in Managed Threads. CancellationTokenSource और CancellationToken से cooperative cancellation की प्रक्रिया पर; cancellation ज़बरदस्ती नहीं सहयोग है और रुकने का तरीका listener तय करता है; polling, callback registration और wait handle तीन निगरानी साधन; ThrowIfCancellationRequested का OperationCanceledException Task cancellation पूर्ण मानता है; linked tokens से कई tokens combine करना; और library को CancellationToken लेने वाली public methods देनी चाहिए। ↩ ↩2 ↩3

  8. Microsoft Learn, Using threads and threading. इस पर कि thread रोकने का सही तरीका CancellationToken है; .NET Core और .NET 5 से Thread.Abort PlatformNotSupportedException फेंकता है, और .NET 5 से compile-time obsolete warning (SYSLIB0006) भी लगती है; cooperative cancellation का जवाब न देने वाले third-party code को ज़बरदस्ती खत्म करने के लिए अलग process में चलाकर Process.Kill से रोकना चाहिए। ↩ ↩2

  9. Microsoft Learn, How to handle cross-thread operations with controls. इस पर कि WinForms control तक पहुँच thread-safe नहीं, कई threads से operations inconsistent state, race, deadlock और freeze लाता है; हर control उसी thread पर बनना और छूआ जाना चाहिए, Windows system messages पहुँचाने के लिए dedicated UI thread माँगता है; और दूसरे thread से Control.Invoke, .NET 9 से Control.InvokeAsync, या BackgroundWorker से सुरक्षित बुलाएँ। ↩ ↩2 ↩3

  10. Microsoft Learn, Threading model (WPF). इस पर कि WPF में UI changes एक thread तक सीमित हैं, background thread UI thread के Dispatcher पर work item register कर request करता है; Dispatcher.Invoke synchronous है जबकि InvokeAsync और BeginInvoke async; और Dispatcher priority queue के रूप में काम process करता है। ↩ ↩2 ↩3

  11. Microsoft Learn, Data Parallelism (Task Parallel Library). इस पर कि Parallel.For / Parallel.ForEach लगभग for loop जितनी लिखत से data parallelism देते हैं; thread बनाने या work item queue करने की ज़रूरत नहीं, मूल loop में lock भी नहीं; और TPL data source कई threads में बाँटता है तथा load टेढ़ा हो तो rebalance करता है। ↩

  12. Microsoft Learn, BlockingCollection<T> Class. इस पर कि BlockingCollection blocking और capacity limit वाला producer/consumer implementation है; capacity limit producer को consumer से बहुत आगे निकलने से रोकती है; और यह async access के लिए design नहीं, async producer/consumer के लिए Channel<T> recommended है। ↩ ↩2

  13. Microsoft Learn, Overview of synchronization primitives. इस पर कि Monitor lock object से mutual exclusion देता है और thread affinity रखता है; C# में Monitor सीधे नहीं lock statement इस्तेमाल होना चाहिए; ReaderWriterLockSlim write exclude करता है और concurrent read देता है; SemaphoreSlim एक process के भीतर हल्का semaphore है, जबकि Semaphore named है और cross-process synchronization के लिए इस्तेमाल हो सकता है। ↩ ↩2 ↩3

  14. Microsoft Learn, Async semaphores, locks, and reader/writer coordination. इस पर कि C# lock statement और Lock type thread affinity रखते हैं इसलिए await के आर-पार इस्तेमाल नहीं हो सकते (await के पहले-बाद continuation चलाने वाला thread बदल सकता है); async code में mutual exclusion के लिए count 1 वाले SemaphoreSlim को WaitAsync और finally में Release से इस्तेमाल करें; और throttling के लिए bounded Channel विकल्प है। ↩

निकटवर्ती विषयों में गहराई से जाने के लिए समान टैग वाले नवीनतम लेख।

ये पृष्ठ विषय को सेवाओं और निर्णयों के व्यापक संदर्भ में रखते हैं।

यह लेख निम्नलिखित सेवाओं से सीधे जुड़ा है।

अक्सर पूछे जाने वाले प्रश्न

इस लेख के विषय पर परामर्श में अक्सर पूछे जाने वाले प्रश्न।

lock(this) या lock(typeof(MyClass)) क्यों न इस्तेमाल करें?
क्योंकि जिस object पर lock लगाते हैं वह आपके code के बाहर भी दिखता है। this आपका instance स्वयं है, इसलिए कोई भी बाहरी code जो उस instance का reference रखे उसी object पर lock ले सकता है — unexpected contention या deadlock का कारण। typeof(MyClass) और ख़तरनाक है: Type object application domain में एक ही होता है, इसलिए आपका lock बिलकुल unrelated code से share हो जाता है। Lock target के रूप में बाहर कभी न उजागर dedicated lock object इस्तेमाल करें। .NET 9 / C# 13 से सिफ़ारिश है dedicated System.Threading.Lock type का instance lock object बनाना।
Thread कितने तक बना सकते हैं? Optimal thread count क्या है?
«Thread count स्वयं तय न करें» — यही आधुनिक उत्तर है। Task और Parallel class इस्तेमाल करें, तो thread pool CPU core count और वर्तमान load के अनुसार parallelism स्वतः adjust करता है। हाथ से new Thread दोहराते design ग्राहक मशीन पर, जहाँ core count अलग हो, अक्सर अधिक या कम threads देते हैं। ध्यान count पर नहीं, काम के प्रकार पर दें: CPU भरने वाली computation core count से अधिक parallel करने पर तेज़ नहीं होती, और मुख्यतः I/O की wait वाला काम thread जोड़ने का विषय ही नहीं — सही कदम async/await से async I/O है।
volatile जोड़ने से कुछ thread-safe हो जाता है?
नहीं। volatile जो guarantee देता है वह ordering है — कि उस field तक पहुँच आसपास की memory operations के सापेक्ष reorder न हो (acquire/release semantics) — «पढ़ो, compute करो, वापस लिखो» जैसी combined operation की atomicity नहीं। उदाहरण के लिए, कई threads volatile int counter पर ++ करें तो जोड़ फिर भी खो जाते हैं। Counter बढ़ाना-घटाना या compare-and-swap के लिए Interlocked class इस्तेमाल करें, और कई variables एक साथ सुरक्षित करने हों तो lock। volatile लगभग केवल सरल state-flag जैसी जगह पर विचार योग्य है — जहाँ एक thread लिखता है और बाकी केवल पढ़ते हैं — और वह flag भी अब CancellationToken से व्यक्त करना standard है।
कभी-कभी ही होने वाला bug multithreading से है या नहीं, कैसे पहचानें?
तीन संकेत संदेह जगाते हैं: «वही operation कभी reproduce होता है कभी नहीं», «debugger लगाने या log जोड़ने पर reproduce रुक जाता है», और «केवल भारी load या startup के तुरंत बाद होता है»। Time-dependent bug की पहचान है कि परिणाम हर run पर बदलता है — यही race condition की परिभाषा है। काटने के लिए पहले हर shared mutable data गिनें, और प्रत्येक के लिए तालिका बनाएँ कि कौन सा lock उसे सुरक्षित करता है। एक भी unprotected access संदिग्ध है। Hang हो तो हर thread का stack लें और देखें कि उनकी lock wait cycle तो नहीं बनाती। Visual Studio debugger में रोककर Parallel Stacks देखें, या production में dump पकड़कर विश्लेषण करें।

लेखक की प्रोफ़ाइल

लेख के लेखक का परिचय पृष्ठ।

Go Komura

KomuraSoft LLC के प्रतिनिधि

Windows सॉफ़्टवेयर विकास, तकनीकी परामर्श और बग जाँच में विशेषज्ञ, विशेष रूप से मौजूदा सिस्टम वाली परियोजनाओं और पुनरुत्पादन में कठिन बग में।

सार्वजनिक लिंक

ब्लॉग पर लौटें