ব্যবহারিক মাল্টিথ্রেডিং সেরা অনুশীলন: .NET সংস্করণ — আরও থ্রেড যোগ করার আগে কী ঠিক করবেন
· Go Komura · Windows, মাল্টিথ্রেডিং, C#, .NET, ব্যবসায়িক অ্যাপ, বাগ তদন্ত, ডিজাইন
«কাজ ধীর, তাই থ্রেড তুলে সমান্তরাল করলাম, আর এখন সমষ্টি মাঝে মাঝে ভুল হয়।» «ব্যাকগ্রাউন্ড কাজ যোগ করলাম, আর অ্যাপ মাসে একবার জমে যায়।» «ডিবাগ চালনায় পুনরুৎপাদন হয় না বলা হয়, কিন্তু গ্রাহকের সাইটে নিশ্চিত ঘটে।» — মাল্টিথ্রেড প্রোগ্রামিংয়ের ভয় এখানেই যে লেখার মুহূর্তে সেটা ঠিক চলছে বলে মনে হয়। রেস-কন্ডিশন বাগ সময়-নির্ভর: পরীক্ষা পেরিয়ে কেবল প্রোডাকশনে মুখ দেখায়।
অন্যদিকে মাল্টিকোর এখন স্বাভাবিক, তাই ব্যবসায়িক অ্যাপেও মাল্টিথ্রেডিং এড়ানো যায় না — «UI না জমিয়ে ভারী কাজ চালানো», «একাধিক যন্ত্র বা ফাইল সমবর্তী প্রক্রিয়া করা»-এর মতো চাহিদা থেকে। গুরুত্বপূর্ণ হলো আরও থ্রেড যোগ করার আগে ডিজাইন নীতি ঠিক করা। মাল্টিথ্রেড বাগ ডিবাগ করে মারা যায় না; ডিজাইনে ঢোকার জায়গাই রাখা হয় না।
এই নিবন্ধ ব্যবহারিক মাল্টিথ্রেডিং সিরিজের .NET সংস্করণ। Windows-এ ব্যবসায়িক অ্যাপ লিখে মাল্টিথ্রেডিং দরকার হয়েছে এমন ডেভেলপারদের লক্ষ্য করে এটি ভাষা বা OS নির্বিশেষে টেকে এমন ডিজাইন নীতি এবং C#/.NET-এর কংক্রিট সরঞ্জাম, আগস্ট ২০২৬ পর্যন্ত প্রাথমিক উৎসের ভিত্তিতে সাজায়। নীতি নিজে Linux-এও C++-এও বদলায় না। নেটিভ কোডে লিখলে একই নীতিকে প্রতি ভাষার সরঞ্জামে নামানো «C++ সংস্করণ» ও «C সংস্করণ» দেখুন, Java-তে লিখলে «Java সংস্করণ»।
1. আগে সিদ্ধান্ত
- প্রথম সেরা অনুশীলন নিজে থ্রেড তৈরি না করা।
new Thread-এর বদলে Task, থ্রেড পুল,Parallel-এর মতো উচ্চস্তরের API-তে চড়ুন, থ্রেড সংখ্যার ব্যবস্থাপনা রানটাইমের হাতে রাখুন।12 - সমান্তরাল করার সময় প্রথমে কাটতে হয় «ভাগ করা পরিবর্তনযোগ্য অবস্থা»। একাধিক থ্রেড একই ভেরিয়েবলে লেখে সেখানেই রেস জন্মায়; লক দিয়ে পাহারা দেওয়ার আগে ডেটা ভাগ, অপরিবর্তনীয়তা ও হস্তান্তর দিয়ে ভাগ করাটাই কমান।3
- লকে শৃঙ্খলা রাখুন। «কোন ডেটা কোন লক রক্ষা করে» এক-এক করে ঠিক করুন, লক অবজেক্ট বাইরে থেকে না দেখা নিবেদিত ইনস্ট্যান্স হোক।
lock(this)ওlock(typeof(X))নিষিদ্ধ। .NET 9 থেকে নিবেদিতSystem.Threading.Lockটাইপ ব্যবহার করুন।4 - থ্রেডগুলোর মধ্যে ডেটা হস্তান্তর কিউ দিয়ে চালান।
System.Threading.Channelsবা কনকারেন্ট কালেকশন দিয়ে প্রডিউসার/কনজিউমার বিন্যাস এদিক-ওদিক লক ছড়ানোর চেয়ে ডিজাইনে সরল, সীমানাও স্পষ্ট।56 - থামার উপায় আগে ডিজাইন করুন। থামানোর একমাত্র সঠিক উত্তর
CancellationTokenদিয়ে সহযোগী ক্যান্সেল;Thread.Abort.NET-এ (Core ধারায়) রানটাইম এক্সেপশন ছোড়ে।78 - UI কেবল UI থ্রেডের। WinForms কন্ট্রোলও WPF এলিমেন্টও যে থ্রেড তৈরি করেছে সে ছাড়া অন্য কেউ ছুঁতে পারবে না। অন্য থ্রেড থেকে
Control.Invoke/Dispatcherদিয়ে অনুরোধ করুন।910 - «সমান্তরাল মানেই দ্রুত» সবসময় সত্য নয়। প্রতি ইটারেশনের কাজ ছোট এমন লুপ সমান্তরালতার ওভারহেডে উল্টো ধীর হতে পারে। কাজে লাগানোর আগে সবসময় মাপুন।3
2. মাল্টিথ্রেডিং কেন কঠিন — রেস কন্ডিশন ও ডেডলক
সংক্ষেপে, মাল্টিথ্রেডিং যে সমস্যা আনে তা দুই ধরনের।4
রেস কন্ডিশন এমন বাগ যেখানে ফল নির্ভর করে একাধিক থ্রেড কোন ক্রমে নির্দিষ্ট কোডখণ্ডে পৌঁছায় তার ওপর। ক্লাসিক উদাহরণ ভাগ করা কাউন্টার: এক লাইন count++ আসলে তিন ধাপে ভাঙে — «পড়া → যোগ → ফিরে লেখা»। দুই থ্রেড এই তিন ধাপ একসঙ্গে চালালে একজনের যোগ অন্যের ফিরে-লেখার ওপর চাপা পড়ে হারায়। ফল চালনায় চালনায় বদলায়, কোন ফল পাবেন তা অনিশ্চিত।4
sequenceDiagram
accTitle: ভাগ করা কাউন্টারের রেস কন্ডিশন
accDescr: ভাগ করা কাউন্টারে বৃদ্ধি হারায় এমন ক্লাসিক রেস কন্ডিশন। count++-এর তিন ধাপের মধ্যে অন্য থ্রেড ঢুকলে যে ফিরে-লেখা শেষ হয় সেটা অন্যটিকে ওভাররাইট করে
participant A as থ্রেড A
participant M as ভাগ করা ভেরিয়েবল count
participant B as থ্রেড B
Note over M: count = 10
A->>M: পড়া (10)
B->>M: পড়া (10)
A->>A: স্থানীয় যোগ (11)
B->>B: স্থানীয় যোগ (11)
A->>M: ফিরে লেখা (11)
B->>M: ফিরে লেখা (11)
Note over M: দুইবার বৃদ্ধি সত্ত্বেও count = 11<br/>থ্রেড A-এর যোগ হারিয়েছে
চিত্র 1: ভাগ করা কাউন্টারে বৃদ্ধি হারায় এমন ক্লাসিক রেস কন্ডিশন। count++-এর তিন ধাপের মধ্যে অন্য থ্রেড ঢুকলে যে ফিরে-লেখা শেষ হয় সেটা অন্যটিকে ওভাররাইট করে
ডেডলক এমন অবস্থা যেখানে দুই থ্রেড প্রত্যেকে অন্যের ধরা লকের অপেক্ষা করে, তাই কেউ এগোতে পারে না। থ্রেড A লক ১ ধরে লক ২-এর অপেক্ষা করে; থ্রেড B লক ২ ধরে লক ১-এর অপেক্ষা করে — এতটুকুই দুজনকে চিরকাল থামানোর জন্য যথেষ্ট।4
flowchart LR
accTitle: ডেডলকের চক্রাকার অপেক্ষা
accDescr: ডেডলকের চক্রাকার অপেক্ষা। অপেক্ষার তীর যখন বলয় গড়ে, সেই বলয়ের প্রতিটি থ্রেড চিরকাল থেমে যায়
A["থ্রেড A<br/>লক ১ ধরে আছে"] -->|"লক ২ ছাড়ার অপেক্ষা"| B["থ্রেড B<br/>লক ২ ধরে আছে"]
B -->|"লক ১ ছাড়ার অপেক্ষা"| A
চিত্র 2: ডেডলকের চক্রাকার অপেক্ষা। অপেক্ষার তীর যখন বলয় গড়ে, সেই বলয়ের প্রতিটি থ্রেড চিরকাল থেমে যায়
দুটোকেই অস্বস্তিকর করে তোলে সময়-নির্ভরতা। ডেভেলপমেন্ট মেশিনে কয়েক হাজার চালনায় একবার লাগে এমন ইন্টারলিভিং (চালনার ক্রমের এক সংমিশ্রণ) গ্রাহকের মেশিনে, আলাদা কোর সংখ্যা ও আলাদা সময় নিয়ে, প্রতিদিন ঘটতে পারে। «ডিবাগার লাগালে পুনরুৎপাদন হয় না» আর «লগ যোগ করলে মিলিয়ে যায়» দুটোই ঘটে কারণ পর্যবেক্ষণ নিজেই সময় বদলায় — এটা রেস-বাগের ক্লাসিক আচরণ।
তাই এখান থেকে প্রতিটি নীতি একদিকেই তাকায়। «সঠিকভাবে সিঙ্ক্রোনাইজ করার» আগে «সিঙ্ক্রোনাইজেশন দরকার এমন জায়গা কমান» — এটাই মাল্টিথ্রেড ডিজাইনের মৌলিক নীতি।
3. নীতি ১: নিজে থ্রেড তৈরি করবেন না
3.1. Task ও থ্রেড পুলে চড়ুন
new Thread(...) দিয়ে সরাসরি থ্রেড তৈরি আজকের .NET-এ ব্যতিক্রমী শেষ উপায়। .NET Framework 4 থেকে মাল্টিথ্রেড ও সমান্তরাল কোডের সুপারিশকৃত উপায় TPL (Task Parallel Library) — অর্থাৎ Task-কে কেন্দ্র করে API পরিবার। TPL সমান্তরালতার মাত্রা উপলব্ধ প্রসেসর অনুযায়ী গতিশীল সামলায়, আর কাজ ভাগ, থ্রেড পুলে শিডিউল, ক্যান্সেল সামলানো, অবস্থা ব্যবস্থাপনা — নিম্নস্তরের সব ঝামেলা নিজে নেয়।1
থ্রেড পুল .NET নিজেই ব্যাপক ব্যবহার করে — Task চালানো, অ্যাসিঙ্ক I/O সম্পূর্ণ করা, টাইমার কলব্যাক ইত্যাদি — আর ছোট কাজ দিলে ডেভেলপারকে থ্রেডের জীবনচক্র সামলাতে হয় না।2
// CPU ব্যবহার করে এমন ভারী কাজ ব্যাকগ্রাউন্ডে
var result = await Task.Run(() => HeavyCalculation(input));
// স্বাধীন একাধিক কাজ সমবর্তী চালিয়ে সবগুলোর অপেক্ষা (সংখ্যা কম হলে)
// * এই রূপ ধরে ProcessAsync I/O-প্রধান অ্যাসিঙ্ক মেথড।
// WhenAll কেবল «ইতিমধ্যে চলা Task-এর অপেক্ষা» করে, তাই CPU গণনা সমবর্তী
// চালাতে চাইলে প্রতিটি কাজ Task.Run(() => Calc(x)) দিয়ে মুড়ে থ্রেড পুলে তুলুন
var results = await Task.WhenAll(items.Select(x => ProcessAsync(x)));
// সংখ্যা বেশি হলে সমবর্তী চালনার সংখ্যায় সীমা লাগিয়ে প্রবাহিত করুন
await Parallel.ForEachAsync(items,
new ParallelOptions { MaxDegreeOfParallelism = 8, CancellationToken = callerCt },
async (x, ct) => await ProcessAsync(x, ct));
// দুটি পয়েন্ট: কলারের টোকেন ParallelOptions-এ যোগ করা
// (ভুললে বডির ct সবসময় None) এবং সেই ct বডিতেও পৌঁছানো (ফেলে দেবেন না)
একটি সতর্কতা আছে। Task.WhenAll(items.Select(...)) গণনার মুহূর্তেই প্রতিটি উপাদানের প্রক্রিয়া একসঙ্গে শুরু করে। কয়েকটা থেকে কয়েক ডজন নির্দিষ্ট কাজ হলে সমস্যা নেই, কিন্তু বড় সংগ্রহে ব্যবহার করলে সকেট, DB সংযোগ ও মেমোরি একবারে শেষ হয়ে যায়। সংখ্যা অনুমান করা যায় না এমন কাজে উপরের Parallel.ForEachAsync-এর মতো সমবর্তী চালনায় সীমা দিন, অথবা পরে বলা bounded চ্যানেল দিয়ে প্রবাহ নিয়ন্ত্রণ করুন।
নিজের থ্রেড তখনই যৌক্তিক যখন থ্রেডের নিজস্ব বৈশিষ্ট্যই শর্ত — «নিবেদিত মেসেজ লুপ লাগে», «থ্রেডিং অ্যাপার্টমেন্ট (STA) নির্দিষ্ট করতে হয়», «অ্যাপের পুরো আয়ু জুড়ে চলতে হয়» — প্রায় কেবল সেই পরিস্থিতিতে।
3.2. ডেটা প্যারালালিজমের জন্য Parallel.For / ForEach
«সংগ্রহের প্রতিটি উপাদানে একই প্রক্রিয়া প্রয়োগ করে পুরো কাজ দ্রুত করতে চাই» — এই ডেটা প্যারালালিজমে লুপ নিজে থ্রেডে ভাগ না করে Parallel.For / Parallel.ForEach ব্যবহার করুন। ডেটা উৎস ভাগ (পার্টিশনিং) ও লোড পুনর্বণ্টন TPL করে, আর মৌলিক লুপে লকও লাগে না।11
তবে অফিসিয়াল ডকুমেন্টেশন দুটি ফাঁদ স্পষ্ট করে বলে।3
- সমান্তরাল সবসময় দ্রুত ভেবে নেবেন না। ইটারেশন কম, বা প্রতিবারের কাজ হালকা, এমন লুপে সমান্তরালতার ওভারহেড মূল কাজ ছাড়িয়ে ধীর করে। কর্মক্ষমতার অনেক উপাদান, তাই সবসময় মাপিয়ে সিদ্ধান্ত নিন।
- ইটারেশনগুলোকে একে অন্যের অপেক্ষা করাবেন না।
Parallel.For-এর প্রতিটি ইটারেশন সত্যি সমান্তরাল চলবে তার কোনো গ্যারান্টি নেই। এক ইটারেশন অন্য ইটারেশনের ইভেন্ট সেটের অপেক্ষা করলে শিডিউলিং অনুসারে ডেডলক হতে পারে।
3.3. «অপেক্ষা» করা কাজ থ্রেডে নয়, অ্যাসিঙ্ক I/O-তে দিন
ফাইল, নেটওয়ার্ক, DB — I/O অপেক্ষাই মূল এমন কাজ থ্রেড বাড়ানোর প্রার্থী নয়। অপেক্ষার সময় এক থ্রেড আটকে রাখা কেবল অপচয়; async/await-এর অ্যাসিঙ্ক I/O অপেক্ষাকালে থ্রেড খরচ করে না। এই ভাগ — CPU-বাউন্ড সমান্তরাল করুন, I/O-বাউন্ড অ্যাসিঙ্ক করুন — মাল্টিথ্রেড ডিজাইনের প্রবেশে প্রথম টানা রেখা।
flowchart TB
accTitle: থ্রেড তোলার আগের শাখা
accDescr: «থ্রেড তোলার» আগের শাখা। বেশিরভাগ ব্যবসায়িক কাজ উপরের তিনটি পথের কোনোটিতে পড়ে, new Thread-এ পৌঁছানো কেবল ব্যতিক্রমী ক্ষেত্র
S["সমান্তরাল করতে চান এমন কাজ আছে"] --> Q1{"কাজের মূল অংশ কী?"}
Q1 -->|"I/O অপেক্ষাই মূল<br/>ফাইল, নেটওয়ার্ক, DB"| ASYNC["async/await-এর অ্যাসিঙ্ক I/O<br/>থ্রেড বাড়াবেন না"]
Q1 -->|"CPU ব্যবহার করে এমন গণনা"| Q2{"কাজের রূপ কী?"}
Q2 -->|"সংগ্রহের প্রতিটি উপাদানে<br/>একই প্রক্রিয়া প্রয়োগ"| PAR["Parallel.For / ForEach"]
Q2 -->|"স্বাধীন এক খণ্ড<br/>ব্যাকগ্রাউন্ড কাজ"| TASK["Task.Run / Task.WhenAll"]
Q2 -->|"মেসেজ লুপ বা STA নির্দিষ্টকরণ ইত্যাদি<br/>থ্রেডের নিজস্ব বৈশিষ্ট্যই শর্ত"| TH["new Thread<br/>(ব্যতিক্রমী শেষ উপায়)"]
চিত্র 3: «থ্রেড তোলার» আগের শাখা। বেশিরভাগ ব্যবসায়িক কাজ উপরের তিনটি পথের কোনোটিতে পড়ে, new Thread-এ পৌঁছানো কেবল ব্যতিক্রমী ক্ষেত্র
async/await-এর ব্যবহারিক সিদ্ধান্ত «C# async/await ব্যবহারিক সিদ্ধান্ত সারণি — Task.Run ও ConfigureAwait»-এ, আর তার তলায় থ্রেড পুল ও অ্যাসিঙ্ক I/O কীভাবে জোড়া «Windows I/O-এর গভীরতা (পর্ব ৩) — I/O Completion Port (IOCP) ও .NET থ্রেড পুল: async/await-এর তলা»-এ বিস্তারিত।
4. নীতি ২: ভাগ করা পরিবর্তনযোগ্য অবস্থা ন্যূনতম করুন
রেস কেবল তখনই ওঠে যখন «একাধিক থ্রেড» ও «ভাগ করা পরিবর্তনযোগ্য ডেটা» দুটোই থাকে। থ্রেডের সংখ্যা প্রয়োজনীয়তা ঠিক করে, তাই ডিজাইন কাটতে পারে ভাগ করা। উপায় তিনটি।
4.1. ভাগ করুন — প্রতিটি থ্রেড কেবল নিজের ডেটা ছুঁক
সবচেয়ে সরল ও শক্তিশালী উপায় ডেটা থ্রেডপ্রতি ভাগ করে দেওয়া। সমান্তরাল লুপের সমষ্টিতে ভাগ করা মোট ভেরিয়েবলে প্রতিবার না লিখে Parallel.For-এর থ্রেড-লোকাল অবস্থা নেওয়া ওভারলোড ব্যবহার করুন: প্রতিটি থ্রেড হাতে উপযোগ গড়ে, শেষে একবার মেলান। ভাগ করা মানে লেখা «প্রতি ইটারেশন» থেকে «প্রতি থ্রেড একবার»-এ নামে, সিঙ্ক্রোনাইজেশন খরচ ও রেসের জানালা দুটোই কয়েক গুণ ছোট হয়।3
long total = 0;
Parallel.For(0, items.Length,
() => 0L, // থ্রেড-লোকাল প্রাথমিক মান
(i, state, local) => local + Weigh(items[i]), // প্রতি ইটারেশন কেবল নিজের local-এ যোগ
local => Interlocked.Add(ref total, local)); // মিলন প্রতি থ্রেডে একবার
flowchart TB
accTitle: থ্রেড-লোকাল সমষ্টি
accDescr: থ্রেড-লোকাল সমষ্টি। প্রক্রিয়াকালে প্রতিটি থ্রেড কেবল নিজের ডেটা ছোঁয় বলে রেসের জায়গা থাকে না, ভাগ করা মানে লেখা মিলনের সময় প্রতি থ্রেডে একবারই হয়
SRC["ডেটা অ্যারে (প্রক্রিয়াকরণের লক্ষ্য)"] --> T1["থ্রেড ১<br/>নিজের অংশ প্রক্রিয়া করে<br/>কেবল স্থানীয় উপযোগে যোগ"]
SRC --> T2["থ্রেড ২<br/>নিজের অংশ প্রক্রিয়া করে<br/>কেবল স্থানীয় উপযোগে যোগ"]
SRC --> T3["থ্রেড ৩<br/>নিজের অংশ প্রক্রিয়া করে<br/>কেবল স্থানীয় উপযোগে যোগ"]
T1 --> M["মিলন: Interlocked.Add দিয়ে<br/>প্রতি থ্রেডে একবারই যোগে প্রতিফলন"]
T2 --> M
T3 --> M
চিত্র 4: থ্রেড-লোকাল সমষ্টি। প্রক্রিয়াকালে প্রতিটি থ্রেড কেবল নিজের ডেটা ছোঁয় বলে রেসের জায়গা থাকে না, ভাগ করা মানে লেখা মিলনের সময় প্রতি থ্রেডে একবারই হয়
4.2. অপরিবর্তনীয় করুন — যা আবার লেখেন না তা নির্বিবাদে ভাগ করা যায়
কেবল পড়া ডেটা যেকোনো সংখ্যক থ্রেড থেকে একসঙ্গে পড়া নিরাপদ। কনফিগ মান, মাস্টার ডেটা, গণনার ইনপুট ইত্যাদি নির্মাণের পর আর না লিখে (ইমিউটেবল করে) সিঙ্ক্রোনাইজেশন ছাড়াই ভাগ করা যায়। C#-এ record টাইপ ও init প্রপার্টি এই ডিজাইনকে সহায়তা করে। কেবল সিদ্ধান্ত যে «বদল লাগলে জায়গায় না লিখে নতুন ইনস্ট্যান্স বানিয়ে বদলান» এমন আরও এক পরিবর্তনযোগ্য অবস্থা সরায় যা অন্যথায় পাহারা দিতে হত।
তবে «রিড-অনলি দেখায়» আর «অপরিবর্তনীয়» আলাদা জিনিস। IReadOnlyList<T>-এর মতো রিড-অনলি ইন্টারফেস মানে কেবল «সেই ইন্টারফেস দিয়ে লেখা যায় না» — পেছনের List<T> অন্য রেফারেন্স থেকে লেখা আটকায় না। record / init-এর গ্যারান্টিও অগভীর: প্রপার্টি যে অবজেক্টে ইশারা করে তা রক্ষা করে না। থ্রেডগুলোর মধ্যে সত্যি নিরাপদে ভাগ করতে চাইলে ImmutableArray<T>-এর মতো System.Collections.Immutable-এর অপরিবর্তনীয় কালেকশন ব্যবহার করুন, অথবা ভাগ করার মুহূর্তে কপি দিন আর লেখার পথই কেটে দিন। তখন শর্ত উপাদানের টাইপ T নিজেও অপরিবর্তনীয়। অপরিবর্তনীয় কালেকশন রক্ষা করে কেবল «ক্রম» — পরিবর্তনযোগ্য উপাদান অবজেক্টের রেফারেন্স আগের মতোই ভাগ থাকে, তাই অন্য পথ দিয়ে উপাদানের ভিতর লেখা গেলে রেস থেকে যায়। অবজেক্ট গ্রাফ পাতা পর্যন্ত অপরিবর্তনীয় করুন, অথবা গভীর কপি দিন।
4.3. হস্তান্তর করুন — ভাগ করার বদলে কিউ দিয়ে পাঠান
তবু থ্রেডগুলোর মধ্যে ডেটা সরাতে হয়। তখন «দুই পক্ষ ভাগ করা ভেরিয়েবল ছোঁয়» নয়, এক পক্ষ লেখে, অন্য পক্ষ পড়ে, মাঝে কিউ — প্রডিউসার/কনজিউমার বিন্যাস নিন।
.NET-এ প্রথম পছন্দ System.Threading.Channels। প্রডিউসার অ্যাসিঙ্ক লেখে, কনজিউমার অ্যাসিঙ্ক পড়ে — FIFO, আর সিঙ্কের সব ঝামেলা চ্যানেল সামলায়।5
var channel = Channel.CreateBounded<WorkItem>(100); // ধারণক্ষমতা 100 — ব্যাকপ্রেশার দেয়
// প্রডিউসার পক্ষ
await channel.Writer.WriteAsync(item, ct); // ভর্তি হলে জায়গা খুলতে অপেক্ষা
// …সব প্রডিউসার লেখা শেষ করলে:
channel.Writer.Complete(); // «আর আসবে না» ঘোষণা। এটা না থাকলে পড়ার লুপ শেষ হতে পারে না
// কনজিউমার পক্ষ
await foreach (var item in channel.Reader.ReadAllAsync(ct))
{
Process(item);
}
flowchart LR
accTitle: চ্যানেলসহ প্রডিউসার/কনজিউমার বিন্যাস
accDescr: চ্যানেলকে মাঝে রেখে প্রডিউসার/কনজিউমার বিন্যাস। দুই পক্ষ ভাগ করা ভেরিয়েবল সরাসরি ছোঁয় না; অপেক্ষা ও ধারণক্ষমতা নিয়ন্ত্রণ চ্যানেলের হাতে
P1["প্রডিউসার ১<br/>WriteAsync"] --> CH["bounded চ্যানেল (ধারণক্ষমতা 100)<br/>FIFO কিউ<br/>সিঙ্ক চ্যানেল সামলায়"]
P2["প্রডিউসার ২<br/>WriteAsync"] --> CH
CH --> C1["কনজিউমার ১<br/>ReadAllAsync"]
CH --> C2["কনজিউমার ২<br/>ReadAllAsync"]
CH -.->|"ভর্তি হলে লেখাকে অপেক্ষা করায়<br/>(ব্যাকপ্রেশার)"| P1
CH -.->|"খালি হলে পড়াকে অপেক্ষা করায়"| C1
চিত্র 5: চ্যানেলকে মাঝে রেখে প্রডিউসার/কনজিউমার বিন্যাস। দুই পক্ষ ভাগ করা ভেরিয়েবল সরাসরি ছোঁয় না; অপেক্ষা ও ধারণক্ষমতা নিয়ন্ত্রণ চ্যানেলের হাতে
ব্যবহারিক গুরুত্ব ধারণক্ষমতা সীমাবদ্ধ (bounded) চ্যানেল বেছে নেওয়া। সীমায় পৌঁছালে ডিফল্ট আচরণ «লেখার পক্ষ জায়গার অপেক্ষা করে», আর সেটাই স্বাভাবিক ব্যাকপ্রেশার। উৎপাদন খরচ ছাড়িয়ে যায় এমন বিন্যাসে সীমাহীন কিউ টাইম বোম: চলতেই থাকে, কিন্তু মেমোরি বাড়ে।5
সিঙ্ক্রোনাস জগতে bounded চ্যানেলের ভূমিকা ধারণক্ষমতা-সহ BlockingCollection<T> পালন করে। ধারণক্ষমতার সীমা উৎপাদককে ভোক্তার অনেক আগে যেতে দেয় না, খালি হলে ভোক্তাকে ব্লক করে অপেক্ষা করায় — ব্লকিং ও ধারণক্ষমতা নিয়ন্ত্রণ দুটোই আছে।12 অন্যদিকে ConcurrentQueue<T> / ConcurrentStack<T> লক ছাড়া কেবল Interlocked অপারেশনে থ্রেড-নিরাপদ দ্রুত কালেকশন6, কিন্তু ধারণক্ষমতার সীমাও «খালি হলে অপেক্ষা» ব্যবস্থাও নেই — কাঁচা থ্রেড-নিরাপদ কিউ। কাজ হস্তান্তরের নায়ক নয়, যন্ত্রাংশ ভেবে নিন। আর BlockingCollection<T> অ্যাসিঙ্ক অ্যাক্সেসের জন্য ডিজাইন নয়, তাই async/await-এর সঙ্গে জোড়াতে Channel<T> নিন।12
আরও একটি সতর্কতা: «ডিকশনারি ConcurrentDictionary-তে বদলালেই থ্রেড-নিরাপদ» এই ধারণা এড়িয়ে চলুন। আলাদা অপারেশন থ্রেড-নিরাপদ হলেও «আছে কি না দেখে তারপর যোগ»-এর মতো যৌগিক অপারেশন তবু রেস করে (GetOrAdd-এর মতো যৌগিক অপারেশনের মেথড ব্যবহার করুন)। সেই GetOrAdd-এও শর্ত আছে: জমা হওয়া মান একটিই নিশ্চিত, কিন্তু মান তৈরির ফ্যাক্টরি ফাংশন প্রতিযোগিতায় একাধিকবার ডাকা হতে পারে। ফ্যাক্টরিতে পার্শ্বপ্রতিক্রিয়া (সংযোগ খোলা, ফাইল তৈরি ইত্যাদি) রাখলে ডুপ্লিকেট চালনায় ফাঁস হয়, তাই ফ্যাক্টরি পার্শ্বপ্রতিক্রিয়ামুক্ত রাখুন, অথবা নিশ্চিত একবারের ইনিশিয়ালাইজেশনে মান হিসেবে Lazy<T> রাখুন। কালেকশনের টাইপ বদলানো ভাগ করা পরিবর্তনযোগ্য অবস্থা কমানোর বিকল্প নয়।
5. নীতি ৩: লকে শৃঙ্খলা রাখুন
ভাগ করা পরিবর্তনযোগ্য অবস্থা কমানোর পরেও প্রায়ই শূন্যে পৌঁছানো যায় না। যা ভাগ থেকে যায় তার জন্য বর্জন (লক) ব্যবহার করুন, কিন্তু লক «সন্দেহজনক জায়গা lock দিয়ে ঘিরে রাখা» সরঞ্জাম নয়। শৃঙ্খলা চারটি।
5.1. «কী রক্ষা করছেন» ঠিক করুন, আর নিবেদিত অবজেক্ট দিয়ে লক করুন
লকের একক «কোডখণ্ড» নয় «ডেটা» ভেবে নিন। রক্ষা করতে চান এমন প্রতিটি পরিবর্তনযোগ্য ডেটা সেটে এক লক অবজেক্ট দিন, আর সেই ডেটা ছোঁয় এমন প্রতিটি জায়গায় সেই লক নিন — এই সারণি ভাঙাই রেস বাগের বাস্তব রূপ।
লক অবজেক্ট বাইরে না খোলা নিবেদিত ইনস্ট্যান্স হোক। lock(this) আপনার ইনস্ট্যান্স রেফার করতে পারে এমন বাহ্যিক কোডের সঙ্গে, lock(typeof(X)) পুরো অ্যাপ্লিকেশন ডোমেনের সঙ্গে লক ভাগ করে — দুটোই ডেডলকের আঁতুড়ঘর। .NET 9 / C# 13 থেকে নিবেদিত টাইপ System.Threading.Lock-এর ইনস্ট্যান্স লক অবজেক্ট করা সুপারিশকৃত।4
public class OrderBook
{
private readonly Lock _gate = new(); // .NET 9+ (তার আগে readonly object)
private readonly List<Order> _orders = []; // _gate যে ডেটা রক্ষা করে
public void Add(Order order)
{
lock (_gate) { _orders.Add(order); }
}
}
C#-এর lock স্টেটমেন্ট এক্সেপশন ছোড়া হলেও লক নির্ভরযোগ্যভাবে ছাড়ে। বিস্তার লক অবজেক্টের টাইপ অনুসারে বদলায়: সাধারণ অবজেক্ট হলে finally-তে Monitor.Exit, Lock টাইপ হলে EnterScope() ও তার ডিসপোজাল।413 অর্থাৎ Lock টাইপের ফিল্ড Monitor-এর আলাদা ব্যবস্থা, আর কোডের কিছু অংশ হাতে Monitor.Enter(_gate) লিখলে lock (_gate)-এর সঙ্গে পারস্পরিক বর্জন দাঁড়ায় না। যে টাইপই হোক, Monitor.Enter / Exit হাতে না লিখে সবসময় lock সিনট্যাক্সে এক করা নিরাপদ।4
5.2. লক ধরে «সময়সাপেক্ষ» বা «বাহ্যিক» কিছু করবেন না
লক যত কমক্ষণ ধরা যায় তত ভালো; ধরে রাখা অবস্থায় কেবল রক্ষিত ডেটা পড়া-লেখাই করা যায়। লক ধরে I/O, ইভেন্ট বা কলব্যাক দিয়ে বাহ্যিক কোডে কল — ধরার সময় কেবল বাড়ায় না, ডাকা পক্ষ অন্য লক নিতে গিয়ে ডেডলকের পথ খোলে। লকের বাইরে প্রস্তুত করুন, লকের ভিতরে কেবল বদলান — মৌলিক রূপ।
আর lock-এর ভিতরে await করা যায় না (কম্পাইল ত্রুটি)। এটা নিষেধ নয়, সুরক্ষা: Monitor-এর থ্রেড অ্যাফিনিটি আছে — যে থ্রেড লক নিয়েছে সেই ছাড়তে হবে — তাই await-এর আগে-পরে থ্রেড বদলাতে পারে এমন অ্যাসিঙ্ক কোডের সঙ্গে মিলে না। অ্যাসিঙ্ক কোডে বর্জনের জন্য প্রাথমিক কাউন্ট 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. একাধিক লক সবসময় একই ক্রমে নিন
লক দুই বা তার বেশি হলে নেওয়ার ক্রম থ্রেড অনুসারে উল্টে যাওয়া ডেডলকের ক্লাসিক প্যাটার্ন। সমাধান সরল: প্রতিটি থ্রেড একই ক্রমে লক নেবে নিয়ম করুন। ক্রম গ্যারান্টি করা যায় না এমন জায়গায় Monitor.TryEnter-এর টাইমআউট ওভারলোড ব্যবহার করুন, নিতে না পারলে ছেড়ে আবার চেষ্টা (অথবা অস্বাভাবিকতা লিখে রাখুন) — চিরকালের হ্যাং শনাক্তযোগ্য ব্যর্থতায় বদলায়।4
5.4. সরল আপডেটে Interlocked, পড়া বেশি হলে ReaderWriterLockSlim
কাউন্টার বাড়ানো-কমানো বা ফ্ল্যাগ বদলানোর মতো এক ভেরিয়েবলের অ্যাটমিক আপডেটে lock-এর চেয়ে Interlocked ক্লাস (Increment / Add / CompareExchange) দ্রুত। প্রতিযোগিতা না থাকলে একটি CPU ইনস্ট্রাকশন প্রিফিক্সেই চলে।4 উল্টোদিকে Interlocked এতদূরই যায়; একাধিক ভেরিয়েবল একসঙ্গে সামঞ্জস্য রাখতে পারে না। volatile-এর সঙ্গে হাতে গড়া লক-ফ্রি কাঠামো মেমোরি মডেলের গভীর বোঝাপড়া লাগে এমন বিশেষজ্ঞের সরঞ্জাম, ব্যবসায়িক অ্যাপে লেখার জিনিস নয়।
«পড়া ঘন, লেখা বিরল» ভাগ করা ডেটার জন্য লেখাই কেবল বর্জন করে পড়া একসঙ্গে চলতে দেয় এমন ReaderWriterLockSlimও আছে।13
6. নীতি ৪: থামার উপায় আগে ডিজাইন করুন
মাল্টিথ্রেড ডিজাইন রিভিউতে প্রথম প্রশ্ন «এটা কীভাবে থামে?»। চালু হওয়া কোড না ভেবেই লেখা যায়, কিন্তু নিরাপদে থামা কোড ডিজাইন না করলে জন্মায় না।
6.1. সহযোগী ক্যান্সেল (CancellationToken) একমাত্র সঠিক উত্তর
.NET-এর থামার মডেল সহযোগী ক্যান্সেল-এ এক করা। থামানোর পক্ষ CancellationTokenSource তৈরি করে তার Token প্রতিটি প্রক্রিয়ায় দেয়। থামাতে চাইলে Cancel() ডাকে। প্রক্রিয়ার পক্ষ টোকেন দেখে, নিজের সুবিধাজনক বিন্দুতে পরিষ্কার করে শেষ করে — জবরদস্তি নয় সহযোগিতা, তাই প্রক্রিয়ার পক্ষ সামঞ্জস্যপূর্ণ অবস্থা রেখেই শেষ হতে পারে।7
private CancellationTokenSource? _cts;
private Task? _worker;
public void Start()
{
if (_worker is { IsCompleted: false }) // চলতে দ্বৈত Start প্রত্যাখ্যান
throw new InvalidOperationException("ওয়ার্কার ইতিমধ্যে চলছে।");
if (_worker is { IsFaulted: true }) // আগের ব্যর্থতা গিলে নতুন না বানানো
throw new InvalidOperationException("আগের ওয়ার্কার ব্যর্থ হয়েছে।", _worker.Exception);
_cts = new CancellationTokenSource();
var token = _cts.Token; // থামার পর আবার Start-এর সঙ্গে রেস না হোক, আগে লোকালে ধরুন
_worker = Task.Run(() => WorkLoop(token), token);
}
private void WorkLoop(CancellationToken ct)
{
while (!ct.IsCancellationRequested) // পোলিং করে দেখা
{
ProcessNextItem(ct); // ব্লক করা কলে ct দিন, তৎক্ষণাৎ বাধার জন্য
}
}
public async Task StopAsync()
{
var cts = _cts; // অপেক্ষাকালে ফিল্ড বদলালেও
var worker = _worker; // ভুল লক্ষ্য না থামাতে লোকালে স্থির করুন
if (cts is null || worker is null) return;
Exception? cancelFailure = null;
try { cts.Cancel(); } // টোকেনে নিবন্ধিত কলব্যাক এক্সেপশন ছুড়তে পারে
catch (Exception ex) { cancelFailure = ex; } // ধরে রাখুন, মিলনের পর জানান
try
{
try { await worker; } // Cancel সফল হোক বা না হোক মিলন নিশ্চিত করুন, মাঝপথের ব্যর্থতাও দেখুন
catch (OperationCanceledException ex) when (ex.CancellationToken == cts.Token)
{ } // আমরা যে ক্যান্সেল চেয়েছি কেবল সেটাকে «স্বাভাবিক» গণ্য করুন
catch (Exception ex) when (cancelFailure is not null)
{
throw new AggregateException(cancelFailure, ex); // দুই ব্যর্থতাই হারাবেন না
}
}
finally
{
cts.Dispose(); // মিলন শেষ সোর্স ফেলুন (WaitHandle ইত্যাদি OS সম্পদ ছাড়ে)।
if (ReferenceEquals(_cts, cts))
{
_cts = null; // ফেলা সোর্স পরের StopAsync ব্যবহার না করুক
_worker = null;
}
}
if (cancelFailure is not null)
throw new AggregateException(cancelFailure);
}
খেয়াল রাখুন, এই Start / StopAsync একই থ্রেড থেকে (যেমন UI থ্রেড) ক্রমে ডাকা ধরে নেওয়া ন্যূনতম গঠন। একাধিক থ্রেড একসঙ্গে জীবনচক্র চালাতে পারে তো Start / StopAsync নিজেই SemaphoreSlim দিয়ে সারিবদ্ধ করুন — ওয়ার্কার রক্ষার আগে ওয়ার্কার ব্যবস্থাপনার অপারেশনই রেস করলে উদ্দেশ্য নষ্ট।
এই ছোট নমুনায় ব্যবহারিক কয়েকটি কৌশলও আছে। প্রথমে Start চলতে দ্বৈত কল প্রত্যাখ্যান করে। নিঃশর্তে _cts ও _worker ওভাররাইট করলে আগের ওয়ার্কারের রেফারেন্স হারায়, থামানোও যায় না মিলনও যায় না এমন «বিপথগামী থ্রেড» পাশাপাশি চলে। জীবনচক্র API (Start/Stop) «একসঙ্গে একটিই» নিজে রক্ষা করুক — এটা মৌলিক। তার ওপর তিনটি। প্রথম, থামার API সম্পূর্ণতার অপেক্ষা করে। Cancel() কেবল ক্যান্সেল «অনুরোধ» করে; ফিরে আসার মুহূর্তে ওয়ার্কার এখনও ProcessNextItem-এর মাঝে থাকতে পারে। কেবল অনুরোধ করে ফিরে আসা Stop() কলার পরিষ্কার শুরু করলেও ওয়ার্কার চলতে থাকে — নতুন রেস। দ্বিতীয়, Task ফেলে না দিয়ে ধরে রাখুন। _ = Task.Run(...) করে ফেললে ওয়ার্কার এক্সেপশনে মরলে কেউ জানে না। তৃতীয়, টোকেন ল্যাম্বডার ভিতরে _cts.Token রেফার না করে লোকাল ভেরিয়েবলে ধরে তারপর দিন। ল্যাম্বডার ভিতরে রেফার করলে মূল্যায়ন হয় চালনার সময়, থামার ঠিক পরে আবার Start হলে পুরনো ওয়ার্কার নতুন টোকেন ধরে ভুল করে। সেই একই টোকেন Task.Run-এর দ্বিতীয় আর্গুমেন্টেও দিলে প্রক্রিয়ার পক্ষ ThrowIfCancellationRequested বা ক্যান্সেল-সচেতন API-এর OperationCanceledException দিয়ে শেষ হলে Task «ব্যর্থ (Faulted)» নয় «ক্যান্সেল (Canceled)» হিসেবে শ্রেণিবদ্ধ হয় (এই উদাহরণের মতো লুপ শর্তে সাধারণভাবে বেরোলে স্বাভাবিক সম্পূর্ণতা)। আরও একটি: StopAsync-এর catch when ফিল্টার দিয়ে কেবল নিজের টোকেন থেকে আসা ক্যান্সেল ধরে। OperationCanceledException নিঃশর্তে গিললে প্রক্রিয়ার ভিতরের অন্য টোকেন (উপাদানপ্রতি টাইমআউট ইত্যাদি) ছোড়া আসল ব্যর্থতাও «থেমেছে তাই ঠিক» দেখায়। তবে টোকেন মিলিয়ে চেনা WorkLoop-এর ভিতরে লিঙ্কড টোকেন (ধারা ৬.১-এর লিঙ্ক সংযোগ) ব্যবহার করলে ভেঙে যায়, কারণ উড়ে আসা এক্সেপশনে লিঙ্ক পক্ষের টোকেন থাকে। সেই বিন্যাসে WorkLoop-এর বেরোনোর মুখে ct.ThrowIfCancellationRequested() ডেকে বাইরের টোকেনে «অনুবাদ» করে বেরোন, অথবা ফিল্টার when (cts.IsCancellationRequested)-এ ঢিলে করে «থামার অনুরোধ চলাকালীন ক্যান্সেল স্বাভাবিক» মেনে নিন — দুটোর একটা ডিজাইন হিসেবে স্পষ্ট বেছে নিন।
flowchart TB
accTitle: সহযোগী ক্যান্সেলের গঠন
accDescr: সহযোগী ক্যান্সেলের গঠন। থামানোর পক্ষ কেবল Cancel() ডাকে; «কখন ও কীভাবে শেষ হবে» প্রতিটি প্রক্রিয়া নিজে ঠিক করে। তাই সামঞ্জস্যপূর্ণ অবস্থা রেখেই থামা যায়
OWNER["থামানোর পক্ষ"] -->|"Cancel() একবার ডাকে"| CTS["CancellationTokenSource"]
CTS -->|"Token দেয়"| W1["ওয়ার্কার প্রক্রিয়া ১"]
CTS -->|"Token দেয়"| W2["ওয়ার্কার প্রক্রিয়া ২"]
CTS -->|"Token দেয়"| W3["লাইব্রেরির<br/>ক্যান্সেল-সচেতন API"]
W1 -->|"IsCancellationRequested পরখ করে<br/>পরিষ্কার করে নিজেই শেষ হয়"| E1["স্বাভাবিক সমাপ্তি"]
W2 -->|"ThrowIfCancellationRequested"| E2["OperationCanceledException<br/>= ক্যান্সেল সম্পূর্ণ বলে গণ্য"]
W3 -->|"অপেক্ষার মাঝেও তৎক্ষণাৎ বাধা"| E3["ক্যান্সেল সম্পূর্ণ"]
চিত্র 6: সহযোগী ক্যান্সেলের গঠন। থামানোর পক্ষ কেবল Cancel() ডাকে; «কখন ও কীভাবে শেষ হবে» প্রতিটি প্রক্রিয়া নিজে ঠিক করে। তাই সামঞ্জস্যপূর্ণ অবস্থা রেখেই থামা যায়
লাইব্রেরি পক্ষের রীতিও স্থির। ক্যান্সেলযোগ্য অপারেশন CancellationToken নেওয়া পাবলিক মেথড দিক, গণনা লুপে নিয়মিত IsCancellationRequested দেখুক অথবা ThrowIfCancellationRequested() ডাকুক। পরেরটি OperationCanceledException ছোড়ে, আর Task সেটাকে «ব্যর্থতা» নয় «ক্যান্সেল সম্পূর্ণ» গণ্য করে। বাইরে থেকে আসা টোকেন ও ভিতরের কারণ (টাইমআউট ইত্যাদি) দুটোতেই থামাতে চাইলে লিঙ্কড টোকেন দিয়ে সংযোগ করুন।7
6.2. Thread.Abort নেই বলে ধরুন
«কথা শোনে না এমন থ্রেড বাইরে থেকে মারা» Thread.Abort .NET Core / .NET 5 থেকে কেবল PlatformNotSupportedException ছোড়ে, আর ব্যবহারই যায় না। থ্রেড কোথায় চলছে না জেনে তাতে এক্সেপশন ছোড়া সম্পদ মুক্তি মাঝপথে কাটে, অবস্থা নষ্ট করে। সহযোগী ক্যান্সেলে সাড়া দেয় না (বা দেওয়ার মতো করে লেখা যায় না) তৃতীয় পক্ষের কোড জোর করে থামাতে হলে আলাদা প্রসেসে চালিয়ে Process.Kill দিয়ে থামানোই অফিসিয়াল নির্দেশ।8
6.3. অপেক্ষায় পোলিং নয়, ওয়েট হ্যান্ডেল ব্যবহার করুন
«ফ্ল্যাগ না ওঠা পর্যন্ত Sleep(100) লুপে অপেক্ষা» CPU ও সাড়া দুটোই নষ্ট করে। থ্রেডগুলোর মধ্যে সংকেতে ManualResetEventSlim বা SemaphoreSlim-এর মতো সিঙ্ক্রোনাইজেশন প্রিমিটিভ আছে, যা সিগনাল না আসা পর্যন্ত থ্রেডকে ঠিকমতো ঘুম পাড়ায়।13 Windows-এ টাইমার নির্ভুলতা ও ইভেন্ট অপেক্ষার বাছাই «Windows-এ Sleep(1)-এর বদলে ইভেন্ট ওয়েট কেন পছন্দ করা উচিত»-এ বিস্তারিত।
7. UI থ্রেডের বিশেষ অবস্থা — Windows ডেস্কটপ অ্যাপের নিয়ম
Windows ডেস্কটপ অ্যাপে সাধারণ নীতির ওপর আরও এক শক্ত বাধা আছে। UI কেবল যে থ্রেড তৈরি করেছে (UI থ্রেড) সেই ছুঁতে পারে — এই নিয়ম।
WinForms কন্ট্রোল থ্রেড-নিরাপদ নয়; একাধিক থ্রেড থেকে চালালে কন্ট্রোল অসামঞ্জস্য অবস্থায় পড়ে, রেস, ডেডলক ও ফ্রিজ জন্মায়। Windows অ্যাপের কাছে সিস্টেম মেসেজ গ্রহণের নিবেদিত এক থ্রেড চায়, আর UI তৈরি ও পরিচালনা সেই থ্রেডে কেন্দ্রীভূত থাকতে হয়।9 WPF-এর গঠন একই: UI এলিমেন্ট বদলাতে পারে কেবল UI থ্রেড।10
অন্য থ্রেড থেকে UI আপডেট করতে চাইলে সরাসরি না ছুঁয়ে «UI থ্রেডে অনুরোধ»-এ বদলান।
flowchart LR
accTitle: UI আপডেটকে অনুরোধে বদলানো
accDescr: UI আপডেটকে «অনুরোধ»-এ বদলান। ব্যাকগ্রাউন্ড থ্রেডের কাজ মেসেজ কিউতে তোলা পর্যন্ত; কন্ট্রোল ছোঁয় সবসময় UI থ্রেড নিজে
OS["Windows<br/>মাউস, কীবোর্ড, পুনর্অঙ্কন"] --> Q["UI থ্রেডের<br/>মেসেজ কিউ"]
BG["ব্যাকগ্রাউন্ড থ্রেড<br/>(ভারী কাজ, যোগাযোগ)"] -->|"Control.Invoke /<br/>Dispatcher.InvokeAsync দিয়ে অনুরোধ"| Q
Q --> UI["UI থ্রেড<br/>কন্ট্রোল ছোঁয়ার একমাত্র থ্রেড"]
BG -.->|"কন্ট্রোল সরাসরি ছোঁয়া"| NG["নিষিদ্ধ<br/>রেস, ডেডলক, ফ্রিজের কারণ"]
চিত্র 7: UI আপডেটকে «অনুরোধ»-এ বদলান। ব্যাকগ্রাউন্ড থ্রেডের কাজ মেসেজ কিউতে তোলা পর্যন্ত; কন্ট্রোল ছোঁয় সবসময় UI থ্রেড নিজে
| ফ্রেমওয়ার্ক | অনুরোধের উপায় |
|---|---|
| WinForms | Control.Invoke (সিঙ্ক) / Control.BeginInvoke (অ্যাসিঙ্ক) / .NET 9 থেকে Control.InvokeAsync9 |
| WPF | Dispatcher.Invoke (সিঙ্ক) / Dispatcher.InvokeAsync / Dispatcher.BeginInvoke (অ্যাসিঙ্ক)10 |
এদের মধ্যে সিঙ্ক রূপ (Control.Invoke / Dispatcher.Invoke) সতর্কতার। UI থ্রেড সেই ওয়ার্কার শেষ হওয়ার সিঙ্ক অপেক্ষা করছে, আর ওয়ার্কার Invoke ডাকলে একে অন্যের অপেক্ষার ডেডলক হয় (ধারা ২-এর চক্রাকার অপেক্ষা ঠিক তাই)। ব্যাকগ্রাউন্ড থেকে বিজ্ঞপ্তি ও অগ্রগতি রিপোর্টে অ্যাসিঙ্ক রূপ (BeginInvoke / InvokeAsync) ডিফল্ট করুন, সিঙ্ক রূপ কেবল সেই পরিস্থিতিতে যেখানে UI থ্রেড আপনার অপেক্ষা করছে না বলা যায়।
ব্যবহারে আরও ভালো উত্তর আছে। UI থ্রেডে শুরু করা কাজ async/await দিয়ে লিখলে await UI থ্রেডের SynchronizationContext ধরে, পরের কাজ স্বয়ংক্রিয়ভাবে UI থ্রেডে আবার চালায়, তাই হাতে Invoke লেখার জায়গা অনেক কমে। তবে এটা নিঃশর্ত বৈশিষ্ট্য নয়। ব্যাকগ্রাউন্ড কলব্যাক থেকে ঢোকা কোড, বা ConfigureAwait(false)-এর পরের কন্টিনিউয়েশন UI থ্রেডে ফেরে না, তাই সেই পথে UI ছুঁলে স্পষ্ট ডিসপ্যাচ এখনও লাগে। «ভারী কাজ Task.Run বা অ্যাসিঙ্ক I/O-তে, ফল স্ক্রিনে দেখানো await-এর পরে» — এই রূপে মেলানো আধুনিক Windows অ্যাপের মৌলিক আকার। UI থ্রেড ও async/await-এর সম্পর্ক «WPF/WinForms-এর async ও UI থ্রেড এক পাতায়»-এ এক ছবিতে সাজানো।
আর Office সংযোগ বা লেগাসি কম্পোনেন্টের মতো COM জড়ালে COM-এর নিজস্ব থ্রেডিং মডেল (STA/MTA) আরও এক স্তর যোগ হয়। «UI থ্রেডে COM অবজেক্ট বানিয়ে অন্য থ্রেড থেকে ডাকতে জমে গেল» দুর্ঘটনা এই স্তরের, «COM STA/MTA মূলকথা - থ্রেডিং মডেল ও হ্যাং কীভাবে এড়াবেন»-এ ব্যাখ্যা।
8. নেটিভ কোডে (C++/C) লিখলে
এখান পর্যন্ত নীতি — থ্রেড সরাসরি তৈরি করবেন না, ভাগ করা পরিবর্তনযোগ্য অবস্থা কমান, লকের শৃঙ্খলা, থামার নকশা — নেটিভ কোডেও সরাসরি প্রযোজ্য। বদলায় সরঞ্জাম। C++-এ RAII ও std::jthread / std::mutex / std::atomic; C-তে Win32 API-এর _beginthreadex, SRW লক, কন্ডিশন ভেরিয়েবল, স্টপ-ইভেন্ট প্যাটার্ন। প্রতিটি এই সিরিজের «C++ সংস্করণ» ও «C সংস্করণ»-এ ভাষা-নির্দিষ্ট ফাঁদসহ (std::thread-এর ডেস্ট্রাক্টর, TerminateThread-এর বিপদ, DllMain ও লোডার লক ইত্যাদি) আলোচিত।
9. যাচাই ও ডিবাগ — এই অনুমানে প্রস্তুতি যে পুনরুৎপাদন হবে না
মাল্টিথ্রেড বাগ টেস্টে পাওয়া যাবে ভরসা করা যায় না। সাধারণ ইউনিট টেস্ট «ঘটনাক্রমে রেস হয়নি» চালনাকে পাস গণে। প্রস্তুতি তিন স্তরে ভাবুন।
প্রথম প্রতিরক্ষা রেখা এখন পর্যন্ত আলোচিত ডিজাইন নীতিগুলো, ঠিক যেমন আছে। ভাগ করা পরিবর্তনযোগ্য অবস্থা পাঁচটি অ্যাপ আর পঞ্চাশটি অ্যাপে সন্দেহের জায়গা দশ গুণ আলাদা। রিভিউতে সারণি দিয়ে দেখুন: «কোন পরিবর্তনযোগ্য ডেটা ভাগ», «কোন লক কোন টুকরো রক্ষা করে», «লক নেওয়ার ক্রম অনন্য কি না», «থামার পথ কোথায়»। যে ডিজাইনের জন্য এই সারণি লিখতে পারেন না সেটা শেষ নয়, এখন যত ভালোই চলুক।
দ্বিতীয়, অস্বাভাবিক অবস্থা লুকানোর বদলে পর্যবেক্ষণযোগ্য করুন। Monitor.TryEnter-এর টাইমআউটে লক অপেক্ষার অস্বাভাবিকতা ধরে লগ করুন,4 থ্রেড পুলে দেওয়া কাজের অদেখা এক্সেপশন গিলে না ফেলে লিখে রাখুন, হ্যাং হলে ফুল ডাম্প নিয়ে সব থ্রেডের স্ট্যাক দেখার ব্যবস্থা রাখুন — «মাঝে মাঝেই ঘটে» বাগের লড়াই সেই একবার থেকে কত তথ্য তোলা যায় তাতেই মীমাংসা হয়। ডাম্প ও লগ সাজানো «Windows অ্যাপ ক্র্যাশে লগ ও ডাম্প রাখার ডিজাইন»-এ আছে।
তৃতীয়, লোডের নিচে ঝাঁকান। কোরের চেয়ে বেশি সমান্তরালতায় দীর্ঘক্ষণ চালানো, প্রক্রিয়াকরণ ক্রম এলোমেলো করা, কৃত্রিম বিলম্ব ঢোকানো — ডেভেলপমেন্ট মেশিনে ইন্টারলিভিংয়ের «জ্যাকপট» লাগানো সহজ করে এমন স্ট্রেস টেস্ট শিপমেন্টের আগে রেস বের করার বাস্তব উপায়। ডিবাগ চালনায় মিলিয়ে যাওয়া বাগ রিলিজ বিল্ড প্লাস ভারী লোডে প্রায়ই পুনরুৎপাদিত হয়।
10. সারাংশ — আরও থ্রেড যোগ করার আগের চেকলিস্ট
মাল্টিথ্রেড প্রোগ্রামিংয়ের সেরা অনুশীলন শেষ বিচারে «সিঙ্ক্রোনাইজেশন সঠিকভাবে লেখার কৌশল» নয়, «সিঙ্ক্রোনাইজেশন না লিখেই চলা ডিজাইন»। শুরুর আগে নিচের আট প্রশ্নের উত্তর দিতে পারলে বড় দুর্ঘটনা প্রায় আটকানো যায়।
- কাজটি CPU-বাউন্ড না I/O-বাউন্ড (দ্বিতীয় হলে উত্তর থ্রেড নয়, async/await)
new Threadলিখতে যাচ্ছেন না তো (Task,Parallel, থ্রেড পুল দিয়ে প্রকাশ যায় কি)- থ্রেডগুলোর মধ্যে ভাগ করা পরিবর্তনযোগ্য ডেটা কোনগুলো, তালিকা করা যায় কি
- সেই ভাগ বিভাজন, অপরিবর্তনীয়তা বা কিউ দিয়ে হস্তান্তর দিয়ে সরানো যায় কি
- বাকি প্রতিটি ভাগ করা ডেটার জন্য মিলিত এক লক ঠিক আছে কি
- লক নেওয়ার ক্রম সব থ্রেডে অনন্য কি, লক ধরে বাহ্যিক কল হচ্ছে না তো
CancellationTokenপ্রতিটি দীর্ঘ কাজ পেয়েছে কি, থামার পথ ব্যাখ্যা করা যায় কি- UI ছোঁয়া কোড UI থ্রেডে কেন্দ্রীভূত কি
মাল্টিথ্রেড বাগ লেখার দিনে দেখা যায় না, ভুলে যাওয়ার পর গ্রাহকের সাইটে দাঁত বের করে। উল্টে বললে, ডিজাইন পর্যায়ে এই চেকলিস্ট পেরোলে «মাঝে মাঝে ক্র্যাশ», «মাসে একবার জমে» — সবচেয়ে ব্যয়বহুল ধরনের বিঘ্ন — কোড লেখার আগেই তুলে ফেলা যায়।
সম্পর্কিত নিবন্ধ
- ব্যবহারিক মাল্টিথ্রেডিং সেরা অনুশীলন: C++ সংস্করণ — RAII ও jthread দিয়ে কাঠামো থেকে দুর্ঘটনা মুছে ফেলা
- ব্যবহারিক মাল্টিথ্রেডিং সেরা অনুশীলন: C সংস্করণ — Win32 API পথে নিরাপদে লেখা
- ব্যবহারিক মাল্টিথ্রেডিং বেস্ট প্র্যাকটিস: Java সংস্করণ — ভার্চুয়াল থ্রেড যুগের রীতি
- C# async/await ব্যবহারিক সিদ্ধান্ত সারণি — Task.Run ও ConfigureAwait
- WPF/WinForms-এর async ও UI থ্রেড এক পাতায়
- Windows I/O-এর গভীরতা (পর্ব ৩) — I/O Completion Port (IOCP) ও .NET থ্রেড পুল: async/await-এর তলা
- COM STA/MTA মূলকথা - থ্রেডিং মডেল ও হ্যাং কীভাবে এড়াবেন
- Windows-এ Sleep(1)-এর বদলে ইভেন্ট ওয়েট কেন পছন্দ করা উচিত
- শেয়ার্ড মেমোরির ফাঁদ ও ব্যবহারিক সেরা অনুশীলন
সম্পর্কিত পরামর্শ ক্ষেত্র
KomuraSoft LLC মাল্টিথ্রেডিংসহ ব্যবসায়িক অ্যাপের ডিজাইন রিভিউ, «মাঝে মাঝে ক্র্যাশ বা জমে যায়»-এর মতো কম পুনরুৎপাদনযোগ্য ত্রুটির মূল কারণ তদন্ত (ডাম্প বিশ্লেষণ ও রেস চিহ্নিতকরণ), এবং বিদ্যমান অ্যাপ সমান্তরাল বা অ্যাসিঙ্ক করার প্রযুক্তিগত পরামর্শ সামলায়। «এই ডিজাইনে রেস হবে কি না দেখে দিন» পর্যায় থেকেই যোগাযোগ করা যায়।
তথ্যসূত্র
-
Microsoft Learn, Task Parallel Library (TPL)। TPL .NET Framework 4 থেকে মাল্টিথ্রেড ও সমান্তরাল কোডের সুপারিশকৃত উপায়; সমান্তরালতার মাত্রা উপলব্ধ প্রসেসর অনুযায়ী গতিশীল সামলায়; কাজ ভাগ, থ্রেড পুলে শিডিউল, ক্যান্সেল সামলানো ও অবস্থা ব্যবস্থাপনা নেয়; প্রতি ইটারেশনের কাজ ছোট লুপ সমান্তরালতার ওভারহেডে ধীর হতে পারে; TPL ব্যবহার করলেও লক, ডেডলক ও রেস কন্ডিশনের মৌলিক বোঝাপড়া সুপারিশকৃত। ↩ ↩2
-
Microsoft Learn, The managed thread pool। ThreadPool ক্লাস সিস্টেম-পরিচালিত ওয়ার্কার থ্রেডের পুল দেয়, ডেভেলপার থ্রেড ব্যবস্থাপনা নয় অ্যাপের কাজে মন দিতে পারেন; .NET TPL অপারেশন, অ্যাসিঙ্ক I/O সম্পূর্ণি, টাইমার কলব্যাক, নিবন্ধিত অপেক্ষা, সকেট সংযোগ ইত্যাদিতে থ্রেড পুল ব্যাপক ব্যবহার করে। ↩ ↩2
-
Microsoft Learn, Potential Pitfalls in Data and Task Parallelism। সমান্তরাল লুপ কখনো ক্রমিকের চেয়ে ধীর হতে পারে তাই সবসময় মাপা উচিত; সমান্তরাল লুপের ভিতরে শেয়ার্ড মেমোরিতে লেখা এড়ানো উচিত, থ্রেড-লোকাল অবস্থা নেওয়া ওভারলোড সুপারিশকৃত; For/ForEach-এর প্রতিটি ইটারেশন সমান্তরাল চলবে তার গ্যারান্টি নেই, ইটারেশনগুলোর মধ্যে অপেক্ষা করা কোড ডেডলক করতে পারে। ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Managed threading best practices। রেস কন্ডিশন (কাউন্টার বৃদ্ধি পড়া-যোগ-ফিরে লেখায় ভেঙে ওভাররাইটে হারায় এমন উদাহরণ) ও ডেডলকের সংজ্ঞা; Thread.Abort না ব্যবহার করে সহযোগী ক্যান্সেল; টাইপ বা this লক অবজেক্ট করা যাবে না এবং .NET 9 / C# 13 থেকে নিবেদিত System.Threading.Lock ইনস্ট্যান্স ব্যবহার; C#-এর lock স্টেটমেন্ট finally-তে Monitor.Exit গ্যারান্টি করে; Monitor.TryEnter-এর টাইমআউটে ডেডলক শনাক্ত; সরল অবস্থা বদলে Interlocked ক্লাস দ্রুত; স্ট্যাটিক ডেটা ডিফল্টে থ্রেড-নিরাপদ ও ইনস্ট্যান্স ডেটা ডিফল্টে নয় — এই ডিজাইন নির্দেশনা। ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10
-
Microsoft Learn, System.Threading.Channels library। চ্যানেল প্রডিউসার/কনজিউমার মডেলের FIFO যা সিঙ্ক ভিতরে সামলায়; CreateBounded দিয়ে ধারণক্ষমতা-সীমাবদ্ধ চ্যানেল তৈরি যায়; সীমায় পৌঁছালে ডিফল্ট আচরণ লেখার পক্ষের অপেক্ষা (Wait), DropOldest ইত্যাদি FullModeও বেছে নেওয়া যায়; লেখা পড়ার চেয়ে দ্রুত হলে ব্যাকপ্রেশার পড়ে। ↩ ↩2 ↩3
-
Microsoft Learn, Thread-safe collections। System.Collections.Concurrent-এর কালেকশন সূক্ষ্ম লক বা লক-ফ্রি ব্যবস্থায় থ্রেড-নিরাপদ; ConcurrentQueue ও ConcurrentStack লক ছাড়া Interlocked অপারেশনে ইমপ্লিমেন্টেড, একাধিক থ্রেড থেকে ঘন যোগ-বিয়োগ সহ্য করে। ↩ ↩2
-
Microsoft Learn, Cancellation in Managed Threads। CancellationTokenSource ও CancellationToken দিয়ে সহযোগী ক্যান্সেল মডেলের ধাপ; ক্যান্সেল জবরদস্তি নয় সহযোগিতা, থামার উপায় শ্রোতা ঠিক করে; পোলিং, কলব্যাক নিবন্ধন ও ওয়েট হ্যান্ডেল — তিন নজরদারি; ThrowIfCancellationRequested-এর OperationCanceledException-কে Task ক্যান্সেল সম্পূর্ণ গণ্য করে; লিঙ্কড টোকেন দিয়ে একাধিক টোকেন সংযোগ; লাইব্রেরি CancellationToken নেওয়া পাবলিক মেথড দেবে। ↩ ↩2 ↩3
-
Microsoft Learn, Using threads and threading। থ্রেড থামাতে CancellationToken ব্যবহার করা উচিত; .NET Core ও .NET 5 থেকে Thread.Abort PlatformNotSupportedException ছোড়ে, .NET 5 থেকে কম্পাইল-সময় অবচয় সতর্কতাও (SYSLIB0006); সহযোগী ক্যান্সেলে সাড়া দেয় না এমন তৃতীয় পক্ষের কোড জোর করে থামাতে আলাদা প্রসেসে চালিয়ে Process.Kill ব্যবহার করা উচিত। ↩ ↩2
-
Microsoft Learn, How to handle cross-thread operations with controls। WinForms কন্ট্রোলে অ্যাক্সেস থ্রেড-নিরাপদ নয়, একাধিক থ্রেড থেকে চালালে অসামঞ্জস্য অবস্থা, রেস, ডেডলক, ফ্রিজ; সব কন্ট্রোল একই থ্রেডে তৈরি ও অ্যাক্সেস হতে হয়, Windows সিস্টেম মেসেজ পৌঁছাতে নিবেদিত UI থ্রেড চায়; অন্য থ্রেড থেকে Control.Invoke, .NET 9 থেকে Control.InvokeAsync, অথবা BackgroundWorker দিয়ে নিরাপদে ডাকা। ↩ ↩2 ↩3
-
Microsoft Learn, Threading model (WPF)। WPF-এ UI বদলানো এক থ্রেডে সীমাবদ্ধ, ব্যাকগ্রাউন্ড থ্রেড UI থ্রেডের Dispatcher-এ কাজ আইটেম নিবন্ধন করে অনুরোধ করে; Dispatcher.Invoke সিঙ্ক, InvokeAsync ও BeginInvoke অ্যাসিঙ্ক; Dispatcher অগ্রাধিকার কিউতে কাজ প্রক্রিয়া করে। ↩ ↩2 ↩3
-
Microsoft Learn, Data Parallelism (Task Parallel Library)। Parallel.For / Parallel.ForEach প্রায় for লুপের মতো লেখায় ডেটা প্যারালালিজম দেয়; থ্রেড তৈরি বা ওয়ার্ক আইটেম কিউ করা লাগে না, মৌলিক লুপে লকও লাগে না; TPL ডেটা উৎস একাধিক থ্রেডে ভাগ করে আর লোড বাঁকা হলে পুনর্বণ্টন করে। ↩
-
Microsoft Learn, BlockingCollection<T> Class। BlockingCollection ব্লকিং ও ধারণক্ষমতা সীমাসহ প্রডিউসার/কনজিউমার ইমপ্লিমেন্টেশন; ধারণক্ষমতার সীমা উৎপাদককে ভোক্তার অনেক আগে যেতে দেয় না; অ্যাসিঙ্ক অ্যাক্সেসের জন্য ডিজাইন নয়, অ্যাসিঙ্ক প্রডিউসার/কনজিউমারের জন্য Channel<T> বিবেচনা করা উচিত। ↩ ↩2
-
Microsoft Learn, Overview of synchronization primitives। Monitor লক অবজেক্ট দিয়ে পারস্পরিক বর্জন দেয় ও থ্রেড অ্যাফিনিটি রাখে; C#-এ Monitor সরাসরি নয় lock স্টেটমেন্ট ব্যবহার করা উচিত; ReaderWriterLockSlim লেখা বর্জন করে পড়া একসঙ্গে চলতে দেয়; SemaphoreSlim প্রসেসের ভিতরে হালকা সেমফোর, Semaphore নামযুক্ত ও ক্রস-প্রসেস সিঙ্ক্রোনাইজেশনে ব্যবহারযোগ্য। ↩ ↩2 ↩3
-
Microsoft Learn, Async semaphores, locks, and reader/writer coordination। C#-এর lock স্টেটমেন্ট ও Lock টাইপের থ্রেড অ্যাফিনিটি আছে তাই await পেরিয়ে ব্যবহার যায় না (await-এর আগে-পরে কন্টিনিউয়েশন চালানো থ্রেড বদলাতে পারে); অ্যাসিঙ্ক কোডে পারস্পরিক বর্জনে কাউন্ট 1-এর SemaphoreSlim, WaitAsync ও finally-তে Release; থ্রটলিংয়ের বিকল্প bounded Channel। ↩
সম্পর্কিত নিবন্ধ
কাছাকাছি বিষয়ে গভীরে যেতে একই ট্যাগযুক্ত সাম্প্রতিক নিবন্ধ।
ব্যবহারিক মাল্টিথ্রেডিং সেরা অনুশীলন: C সংস্করণ — Win32 API পথে নিরাপদে লেখা
Win32-এর সাথে C-তে মাল্টিথ্রেডিংয়ের স্থিত পথ হল _beginthreadex দিয়ে থ্রেড তৈরি, SRW লক ও কন্ডিশন ভেরিয়েবল, Interlocked ফাংশন, এবং স্টপ...
ব্যবহারিক মাল্টিথ্রেডিং সেরা অনুশীলন: C++ সংস্করণ — RAII ও jthread দিয়ে কাঠামো থেকে দুর্ঘটনা মুছে ফেলা
C++-এ মাল্টিথ্রেডিং এমন জগৎ যেখানে ডেটা রেসই অনির্ধারিত আচরণ। এই নিবন্ধ std::thread-এর ডেস্ট্রাক্টরের ফাঁদ, jthread ও stop_token দিয়ে থা...
C# ও PowerShell থেকে WMI/CIM ব্যবহার — হার্ডওয়্যার তথ্য, প্রক্রিয়া নজরদারি ও দূরবর্তী অনুসন্ধানের বাস্তব নির্দেশিকা
PC-র সিরিয়াল নম্বর, ডিস্কের খালি জায়গা ও প্রক্রিয়া চালু হওয়া শনাক্তের নিয়মিত উপায় WMI/CIM। Get-CimInstance-এর মতো CIM কমান্ডলেট, পু...
Spurious Wakeup — কন্ডিশন ভেরিয়েবল কেন "নোটিফাই না হয়েও" জাগে এবং Windows-এ সঠিকভাবে কীভাবে অপেক্ষা করবেন
কন্ডিশন ভেরিয়েবলের ওয়েট নোটিফিকেশন না এলেও ফিরতে পারে (spurious wakeup)। এই নিবন্ধ Windows বাস্তবায়ন থেকে ব্যাখ্যা করে স্পেসিফিকেশন কে...
ব্যবহারিক মাল্টিথ্রেডিং বেস্ট প্র্যাকটিস: Java সংস্করণ — ভার্চুয়াল থ্রেড যুগের রীতি
Java-তে মাল্টিথ্রেডিংয়ের প্রতিষ্ঠিত রীতি হলো থ্রেড সরাসরি না তৈরি করে ExecutorService ও ভার্চুয়াল থ্রেডে চড়া। এই নিবন্ধ ব্যবহারিক নীতি...
সম্পর্কিত বিষয়
এই পৃষ্ঠাগুলো বিষয়টিকে সেবা ও সিদ্ধান্তের বৃহত্তর প্রেক্ষাপটে স্থাপন করে।
Windows-এর প্রযুক্তিগত বিষয়
Windows ডেভেলপমেন্ট, বাগ তদন্ত ও বিদ্যমান সম্পদ ব্যবহারের প্রবেশদ্বার।
এই বিষয়ের সাথে সম্পর্কিত সেবা
নিবন্ধটি নিচের সেবাগুলোর সাথে সরাসরি সম্পর্কিত।
Windows অ্যাপ ডেভেলপমেন্ট
ব্যবসায়িক অ্যাপ, ডিভাইস ইন্টিগ্রেশন ও যোগাযোগ টুল, চাহিদা থেকে ডেভেলপমেন্ট পর্যন্ত।
প্রায়শ জিজ্ঞাসিত প্রশ্ন
এই নিবন্ধের বিষয়ে পরামর্শে প্রায়ই আসা প্রশ্ন।
- lock(this) বা lock(typeof(MyClass)) কেন এড়াবেন?
- কারণ যে অবজেক্টে লক করছেন তা আপনার কোডের বাইরে থেকেও দেখা যায়। this নিজের ইনস্ট্যান্স, তাই সেই ইনস্ট্যান্স রেফার করতে পারে এমন যেকোনো বাহ্যিক কোড একই অবজেক্টে লক করতে পারে, আর অনিচ্ছাকৃত প্রতিযোগিতা বা ডেডলক জন্মায়। typeof(MyClass) আরও বিপজ্জনক: অ্যাপ্লিকেশন ডোমেনে Type অবজেক্ট একটিই, তাই সম্পূর্ণ অসম্পর্কিত কোডের সঙ্গে লক ভাগ হয়ে যায়। লক অবজেক্ট হিসেবে বাইরে না খোলা নিবেদিত অবজেক্ট ব্যবহার করুন। .NET 9 / C# 13 থেকে সুপারিশ হলো নিবেদিত System.Threading.Lock টাইপের ইনস্ট্যান্সকে লক অবজেক্ট করা।
- থ্রেড কতটি তৈরি করা যায়? সর্বোত্তম সংখ্যা কী?
- আধুনিক উত্তর «থ্রেড সংখ্যা নিজে ঠিক করবেন না»। Task ও Parallel ক্লাস ব্যবহার করলে থ্রেড পুল CPU কোর সংখ্যা ও লোড অনুযায়ী সমান্তরালতার মাত্রা নিজে সামলায়। হাতে new Thread বারবার ডাকা ডিজাইন আলাদা কোর সংখ্যার গ্রাহক মেশিনে অতিরিক্ত বা অপ্রতুল হয়ে যায়। খেয়াল রাখতে হবে সংখ্যা নয়, কাজের ধরন: CPU পুরো খরচ করা গণনা কোরের চেয়ে বেশি সমান্তরাল করলে দ্রুত হয় না, আর I/O অপেক্ষাই মূল এমন কাজে থ্রেড বাড়ানোর প্রশ্নই ওঠে না — সঠিক পথ async/await-এর অ্যাসিঙ্ক I/O।
- volatile যোগ করলে কি কিছু থ্রেড-নিরাপদ হয়?
- না। volatile যা গ্যারান্টি দেয় তা ক্রম — সেই ফিল্ডে অ্যাক্সেস আশপাশের মেমোরি অপারেশনের সঙ্গে পুনর্বিন্যস্ত হবে না (acquire/release সেমান্টিক্স) — «পড়ুন, হিসাব করুন, ফিরিয়ে লিখুন»-এর মতো যৌগিক অপারেশনের অবিভাজ্যতা নয়। উদাহরণ: volatile int কাউন্টারে একাধিক থ্রেড থেকে ++ করলেও বৃদ্ধি হারায়। কাউন্টার বাড়াতে-কমাতে বা তুলনা করে বদলাতে Interlocked ক্লাস, আর একাধিক ভেরিয়েবল একসঙ্গে রক্ষা করতে lock ব্যবহার করুন। volatile প্রায় কেবল থামার ফ্ল্যাগের মতো সরল ক্ষেত্রে বিবেচ্য — এক থ্রেড লেখে, অন্যরা কেবল পড়ে — আর সেই ফ্ল্যাগও এখন CancellationToken দিয়ে প্রকাশ করাই মানক।
- মাঝে মাঝেই ঘটে এমন বাগ মাল্টিথ্রেডিং থেকে কি না কীভাবে বুঝব?
- সন্দেহের তিন লক্ষণ: «একই কাজ কখনো পুনরুৎপাদিত হয়, কখনো হয় না», «ডিবাগার লাগালে বা লগ যোগ করলে আর দেখা যায় না», আর «কেবল ভারী লোডে বা চালু হওয়ার ঠিক পরে ঘটে»। সময়-নির্ভর বাগের বৈশিষ্ট্য চালনায় চালনায় ফল বদলানো — সেটাই রেস কন্ডিশনের সংজ্ঞা। আলাদা করতে প্রথমে ভাগ করা প্রতিটি পরিবর্তনযোগ্য ডেটা তালিকা করুন, আর প্রত্যেকটি «কোন লক রক্ষা করে» সারণিতে লিখুন। একটিও অরক্ষিত অ্যাক্সেস থাকলে সেটাই সন্দেহভাজন। হ্যাং হলে সব থ্রেডের স্ট্যাক নিন, দেখুন তারা একে অন্যের লকের অপেক্ষায় চক্র গড়ছে কি না। Visual Studio ডিবাগারে থামিয়ে Parallel Stacks দেখুন, অথবা প্রোডাকশনে ডাম্প নিয়ে বিশ্লেষণ করুন।