ব্যবহারিক মাল্টিথ্রেডিং সেরা অনুশীলন: C++ সংস্করণ — RAII ও jthread দিয়ে কাঠামো থেকে দুর্ঘটনা মুছে ফেলা

· · Windows, মাল্টিথ্রেডিং, C++, Visual Studio, ব্যবসায়িক অ্যাপ, বাগ তদন্ত, ডিজাইন

«C#-এ ভালো চলা ডিজাইন C++-এ পোর্ট করার পর মাঝে মাঝে ক্র্যাশ করতে লাগল।» «আমরা std::thread ব্যবহার করলাম, আর এক্সেপশন ছোড়া হলে পুরো অ্যাপ terminate দিয়ে তৎক্ষণাৎ মারা গেল।» «volatile bool ফ্ল্যাগ দিয়ে থামাচ্ছিলাম, কিন্তু কেবল রিলিজ বিল্ডেই থামত না।» — C++ মাল্টিথ্রেডিং এমন বিপদ আনে যা ম্যানেজড ভাষায় নেই: ডেটা রেস, যেমন আছে, অনির্ধারিত আচরণ (UB)। কেবল নষ্ট মান পড়ার কথা নয়; কম্পাইলারের অপ্টিমাইজেশন অনুমান ভেঙে পড়ে, আর আপনি এমন অবস্থায় পৌঁছান যেখানে আক্ষরিক অর্থে যেকোনো কিছু ঘটতে পারে।

এই নিবন্ধ আমাদের ব্যবহারিক মাল্টিথ্রেডিং সিরিজের C++ সংস্করণ। আধুনিক C++ (C++17/20)-এ ব্যবসায়িক অ্যাপ, সরঞ্জাম নিয়ন্ত্রণ সফটওয়্যার ও DLL লেখেন এমন ডেভেলপারদের জন্য, এটি মাল্টিথ্রেড ডিজাইনের সাধারণ নীতি — থ্রেড সরাসরি বাড়াবেন না, ভাগ করা পরিবর্তনযোগ্য অবস্থা কমান, লক শৃঙ্খলা প্রয়োগ করুন, থামার উপায় আগে ডিজাইন করুন — C++ ও Windows-এর সরঞ্জামে নামিয়ে আনে, C++-নির্দিষ্ট ফাঁদসহ, আগস্ট ২০২৬ পর্যন্ত প্রাথমিক উৎসের ভিত্তিতে। একা দাঁড়ানোর জন্য লেখা। অন্য ভাষায় বিস্তৃত একই নীতি «.NET সংস্করণ», «C সংস্করণ» ও «Java সংস্করণ»-এও আছে।

1. আগে সিদ্ধান্ত

  • C++-এ ডেটা রেস «নষ্ট মান পড়তে পারেন» নয় — সেটা অনির্ধারিত আচরণ। কোডে একটিও অসিঙ্ক্রোনাইজড ভাগ করা পরিবর্তনযোগ্য অ্যাক্সেস না রাখা পরম শর্ত, অন্য ভাষার চেয়ে বেশি।1
  • std::thread খালি ব্যবহার করবেন না। থ্রেড এখনও joinable থাকতে std::thread-এর ডেস্ট্রাক্টর চললে std::terminate প্রসেসকে তৎক্ষণাৎ মারে। C++20-এর std::jthread ডেস্ট্রাক্টরে স্বয়ংক্রিয় join করে এবং থামার অনুরোধ ব্যবস্থা (stop_token) তৈরি থাকে।23
  • লক সবসময় RAII দিয়ে ধরুন। হাতে mtx.lock() লেখা বন্ধ করুন; lock_guard / scoped_lock ব্যবহার করুন। এক্সেপশন ছোড়া হলেও ডেস্ট্রাক্টর নির্ভরযোগ্যভাবে লক ছাড়ে। একাধিক লক একসঙ্গে নিলে scoped_lock ডেডলক-এড়ানো অ্যালগরিদম দিয়ে সামলায়।4
  • volatile সিঙ্ক্রোনাইজেশনের সরঞ্জাম নয়। ভাগ করা ফ্ল্যাগ ও কাউন্টারের জন্য std::atomic, একাধিক ভেরিয়েবল একসঙ্গে রক্ষা করতে std::mutexstd::atomic অবিভাজ্যতা ও memory_order-ভিত্তিক ক্রম দুটোই দেয়।5
  • রendezvous অপেক্ষা condition_variable-এর প্রেডিকেট-রূপ wait দিয়ে করুন। কন্ডিশন ভেরিয়েবল স্পিউরিয়াস ওয়েকআপের (বিজ্ঞপ্তি ছাড়া জাগা) অধীন, তাই প্রেডিকেট ছাড়া wait বাগের আঁতুড়ঘর।6
  • থ্রেড কীভাবে থামাবেন তার মৌলিক রূপ jthread + stop_token (C++20)। তার আগের পরিবেশে std::atomic<bool> প্লাস কন্ডিশন ভেরিয়েবল দিয়ে সহযোগী থামানো হাতে গড়ুন। থ্রেড জোর করে শেষ করা C++ জগতে নেই বলেই ধরুন।3
  • std::async ব্যবহারের আগে জানুন future-এর ডেস্ট্রাক্টর ব্লক করতে পারে। ফেরত মান ফেলে দিলে সিরিয়াল এক্সিকিউশনের মতোই প্রভাব পায়।7
  • Win32 সিঙ্ক্রোনাইজেশন অবজেক্ট কেবল «Win32 অপেক্ষা API-এর সঙ্গে কাজ» ও «ক্রস-প্রসেস» পরিস্থিতিতে জায়গা পায়। অন্যত্র স্ট্যান্ডার্ড লাইব্রেরির বিরুদ্ধে লেখা পোর্টেবিলিটি ও রক্ষণাবেক্ষণের জন্য ভালো।8

2. মাল্টিথ্রেডিং কেন কঠিন — রেস কন্ডিশন, ডেডলক ও অনির্ধারিত আচরণ

সংক্ষেপে, মাল্টিথ্রেডিং যে সমস্যা আনে তা ভাষা নির্বিশেষে দুই ধরনের।

রেস কন্ডিশন এমন বাগ যেখানে ফল নির্ভর করে একাধিক থ্রেড কোন ক্রমে নির্দিষ্ট কোডখণ্ডে পৌঁছায় তার ওপর। ক্লাসিক উদাহরণ ভাগ করা কাউন্টার: একক এক্সপ্রেশন ++count মেশিন-কোড স্তরে তিন ধাপে ভাঙে — পড়া, যোগ, ফিরে লেখা। দুই থ্রেড সেই তিন ধাপে একসঙ্গে ঢুকলে এক থ্রেডের যোগ অন্যের ফিরে-লেখার ওপর চাপা পড়ে হারিয়ে যায়। ফল চালনায় চালনায় বদলায়, কোন ফল পাবেন তা অনিশ্চিত।

থ্রেড Bভাগ করা ভেরিয়েবল countথ্রেড Aথ্রেড Bভাগ করা ভেরিয়েবল countথ্রেড Acount = 10দুইবার বৃদ্ধি ঘটেছে,তবু count = 11 — থ্রেড A-এর যোগ হারিয়েছেপড়া (10)পড়া (10)স্থানীয় যোগ (11)স্থানীয় যোগ (11)ফিরে লেখা (11)ফিরে লেখা (11)

চিত্র 1: ক্লাসিক রেস কন্ডিশন যেখানে ভাগ করা কাউন্টারে বৃদ্ধি হারায়। ++count-এর তিন ধাপের মধ্যে অন্য থ্রেড ঢুকলে যে ফিরে-লেখা শেষ হয় সেটা অন্যটিকে ওভাররাইট করে

ডেডলক এমন অবস্থা যেখানে দুই থ্রেড প্রত্যেকে অন্যের ধরা লকের অপেক্ষা করে, তাই কেউ এগোতে পারে না। থ্রেড A লক ১ ধরে লক ২-এর অপেক্ষা করে; থ্রেড B লক ২ ধরে লক ১-এর অপেক্ষা করে — এতটুকুই দুজনকে চিরকাল থামানোর জন্য যথেষ্ট।

লক ২ নেওয়ার অপেক্ষালক ১ নেওয়ার অপেক্ষাথ্রেড Aলক ১ ধরেথ্রেড Bলক ২ ধরে

চিত্র 2: ডেডলকের চক্রাকার অপেক্ষা। অপেক্ষার তীর যখন বলয় গড়ে, সেই বলয়ের প্রতিটি থ্রেড চিরকাল থেমে যায়

দুটোকেই অস্বস্তিকর করে তোলে সময়-নির্ভরতা। ডেভেলপমেন্ট মেশিনে কয়েক হাজার চালনায় একবার লাগে এমন ইন্টারলিভিং গ্রাহকের মেশিনে, আলাদা কোর সংখ্যা ও আলাদা সময় নিয়ে, প্রতিদিন ঘটতে পারে। «ডিবাগার লাগালে পুনরুৎপাদন হয় না» আর «লগ যোগ করলে মিলিয়ে যায়» দুটোই ঘটে কারণ পর্যবেক্ষণ নিজেই সময় বদলায় — এটা রেস-বাগের ক্লাসিক আচরণ। তাই এই নিবন্ধের প্রতিটি নীতি একদিকেই তাকায়: সঠিকভাবে সিঙ্ক্রোনাইজ করার চিন্তার আগে, সিঙ্ক্রোনাইজেশন দরকার এমন জায়গা কমান।

2.1. C++-এ ডেটা রেস সরাসরি অনির্ধারিত আচরণ

তার ওপরে C++-এ আরও এক স্তর আছে যা অন্য ভাষায় নেই। C++ স্ট্যান্ডার্ড অনুসারে, একাধিক থ্রেড সিঙ্ক্রোনাইজেশন ছাড়া একই মেমোরি অবস্থানে গেলে এবং অন্তত একজন লিখলে সেটা ডেটা রেস, আর সেটা অনির্ধারিত আচরণ। C++ Core Guidelines-এর কনকারেন্সি অধ্যায় (CP.2, «Avoid data races») এটিকে প্রথম পরম নিয়ম হিসেবে রাখে।1 অনির্ধারিত আচরণ «পুরনো মান বা নতুন পড়তে পারেন» এমন নরম গল্প নয়। কম্পাইলার এই প্রেক্ষাপটে অপ্টিমাইজ করে যে ডেটা রেস নেই, তাই সোর্স থেকে কখনো অনুমান করা যায় না এমন আচরণ — লুপ থেকে শর্ত পরীক্ষা উধাও হওয়া, লেখার পুনর্বিন্যাস বা মিলন — বৈধভাবে ঘটে। ক্লাসিক দুর্ঘটনা যেখানে «volatile bool থামার ফ্ল্যাগ কেবল রিলিজ বিল্ডেই কাজ করে না» ঠিক এর পাঠ্যপুস্তক উদাহরণ।

2.2. RAII ভিত্তি

C++-নির্দিষ্ট আরও একটি প্রেক্ষাপট এক্সেপশন ও রিসোর্স ব্যবস্থাপনা। C++-এ finally নেই; তার জায়গায় RAII (ডেস্ট্রাক্টর দিয়ে স্বয়ংক্রিয় মুক্তি), আর মাল্টিথ্রেডিংয়ের সরঞ্জাম এই অনুমানে ডিজাইন যে আপনি তা ব্যবহার করবেন। «লক অবজেক্টের আয়ু দিয়ে সামলান»; «থ্রেডের join-ও অবজেক্টের আয়ু দিয়ে নিশ্চিত করুন» — সেই রীতিতে চলা নিরাপদ মাল্টিথ্রেডেড C++ লেখার ভিত্তি।

3. থ্রেড কীভাবে শুরু করবেন — thread-এর ফাঁদ, আর jthread

3.1. std::thread-এর ডেস্ট্রাক্টর «দুর্ঘটনা ঘটানোর জন্য ডিজাইন»

std::thread-এর বিখ্যাত ফাঁদ আছে। থ্রেড এখনও joinable থাকতে (joinও হয়নি detachও হয়নি) ডেস্ট্রাক্টর চললে std::terminate ডাকা হয় আর প্রসেস তৎক্ষণাৎ মরে।9

void process()
{
    std::thread worker([]{ HeavyWork(); });
    DoSomething();      // ← এখানে এক্সেপশন ছোড়া হলে...
    worker.join();      // ← join কখনো পৌঁছায় না; worker-এর ডেস্ট্রাক্টর terminate ডাকে
}

এটিকে এক্সেপশন-নিরাপদ করতে try/catch দিয়ে join নিশ্চিত করতে হত — RAII ভাষায় বিকৃত অবস্থা যেখানে কেবল থ্রেড হাতে সামলাতে হত। C++20-এর std::jthread এটা সমাধান করে। কারণ তার ডেস্ট্রাক্টর স্বয়ংক্রিয়ভাবে থামার অনুরোধ দেয় তারপর join করে, উপরের কোড std::jthread-এ স্যুইচ করলেই এক্সেপশন-নিরাপদ হয়ে যায়।2 MSVC-তে <stop_token>jthread Visual Studio 2019 16.9 থেকে পাওয়া যায়।3

std::threadjoinও হয়নি detachও হয়নিstd::threadআগেই joinstd::jthread - C++20থ্রেড শুরু হয়েছেস্কোপ বেরোনোর সময়কী হয়?std::terminateপ্রসেস তৎক্ষণাৎ মরেনিরাপদ joinস্বয়ংক্রিয় request_stop + joinএক্সেপশন ছোড়া হলেও নিরাপদ

চিত্র 3: থ্রেড অবজেক্টের আয়ু ও কীভাবে শেষ হয়। std::thread এমনভাবে নির্দিষ্ট যে join ভুললে তৎক্ষণাৎ মরে, তাই C++20 থেকে jthread-কে ডিফল্ট করুন

নিয়ম হিসেবে detach() ব্যবহার করবেন না। join-এর সব উপায় হারানো থ্রেড শাটডাউন ক্র্যাশের ক্লাসিক কারণ হয়, প্রসেস বেরোনোর সময় স্ট্যাটিক ভেরিয়েবল ও হিপ ধ্বংসের সঙ্গে দৌড়ে।

3.2. «থ্রেড স্তরের ওপরে» সরঞ্জাম — async, future ও সমান্তরাল অ্যালগরিদম

.NET সংস্করণের নীতি «নিজে থ্রেড তৈরি করবেন না» C++-এ নিচের সরঞ্জামে ম্যাপে পড়ে।

  • std::async + std::future: একবারের অ্যাসিঙ্ক কাজ ও তার ফল পাওয়ার জন্য। তবে গুরুত্বপূর্ণ বৈশিষ্ট্য: std::async দিয়ে চালু কাজের সঙ্গে বাঁধা future (বা শেষ shared_future) কাজ অসম্পূর্ণ থাকতে ডেস্ট্রাক্টর চললে সম্পূর্ণ পর্যন্ত ব্লক করে7 সত্যি std::launch::async দিয়ে চালু কাজের জন্য ফেরত future ফেলে দেওয়া সেই বিন্দুতে সিঙ্ক্রোনাস এক্সিকিউশনের সমান। আরও খারাপ, লঞ্চ পলিসি না দিলে ইমপ্লিমেন্টেশন ডিফল্টে deferred (অলস এক্সিকিউশন) বেছে নিতে পারে, তখন কেউ get() / wait() না ডাকলে কাজ চলেই না এবং চুপচাপ উধাও হয়। সমবর্তী এক্সিকিউশন নিশ্চিত করতে চাইলে std::launch::async স্পষ্ট করুন, আর মালিক future-এর আয়ু সামলাক।
  • PPL - Parallel Patterns Library - concurrency::parallel_for / parallel_for_each: সংগ্রহের প্রতিটি উপাদানে সমান্তরালে কাজ প্রয়োগ। তবে এক ইটারেশনের কাজ খুব ছোট হলে fork/join ওভারহেড লাভ খেয়ে ফেলে, তাই নিয়মত বাইরের লুপে সমান্তরাল করুন।10
  • C++17-এর সমান্তরাল অ্যালগরিদম - std::execution::par: MSVC-তে প্রধান অ্যালগরিদম সমান্তরাল (সব নয়)।11 খেয়াল রাখুন এক্সিকিউশন পলিসির নিচে উপাদান প্রক্রিয়াকরণ থেকে এক্সেপশন বেরোলে std::terminate ডাকা হয়। কলব্যাকের ভিতরে নিজের এক্সেপশন সীমা (try/catch) রাখা ধারা ৬-এর থ্রেড সীমার মতোই চিন্তা।

অন্যত্র টানা রেখা — «I/O অপেক্ষা থ্রেড বাড়িয়ে সমাধান নয়» — অপরিবর্তিত প্রযোজ্য। নেটিভ Windows কোডে OVERLAPPED I/O ও IOCP সেই কাজ ধরে (যন্ত্রপাতির জন্য দেখুন «Windows I/O-এর গভীরতা, পর্ব ২»)।

4. ভাগ করা পরিবর্তনযোগ্য অবস্থা কমানো — ভাগ, মানে পাস, const ও কিউ

প্রতিযোগিতা কেবল তখনই ওঠে যখন «একাধিক থ্রেড» ও «ভাগ করা পরিবর্তনযোগ্য ডেটা» দুটোই থাকে। থ্রেডের সংখ্যা প্রয়োজনীয়তা ঠিক করে, তাই ডিজাইন কাটতে পারে ভাগ করা। উপায় তিন পরিবারে পড়ে — ভাগ করা, অপরিবর্তনীয় করা, ও ডেটা হস্তান্তর — আর C++-এ প্রতিটি এভাবে লেখা হয়।

ভাগ করুন। সমান্তরাল সমষ্টির মতো কাজে প্রতিটি থ্রেড ভাগ করা যোগে লেখার বদলে প্রতিটি থ্রেডকে নিজের স্থানীয় উপযোগ দিন আর শেষে একবার মেলান। ভাগ করা মানে লেখা «প্রতি ইটারেশন» থেকে «প্রতি থ্রেড একবার»-এ নামে, সিঙ্ক্রোনাইজেশন খরচ ও প্রতিযোগিতার জানালা দুটোই কয়েক গুণ কমে। সেই এক মিলন ধাপ std::mutex দিয়ে বা std::atomic-এ fetch_add দিয়ে — দুটোই ঠিক।

মানে পাস করুন। থ্রেডের দরকারি ডেটা শুরুতে কপি (বা মুভ) করে দিলে সেই ডেটা থ্রেডের একান্ত হয়, সিঙ্ক্রোনাইজেশন লাগে না। ল্যাম্বডা রেফারেন্সে ধরা ([&]) তারপর শেষ হয়ে যাওয়া ভেরিয়েবল ছোঁয়া সাধারণ দুর্ঘটনা, তাই থ্রেডে দেওয়া ল্যাম্বডা স্পষ্ট ক্যাপচার, নিয়মত কপি বা মুভ ব্যবহার করুক। তবে «কপি হয়েছে, তাই একান্ত» কেবল তখনই যখন মান পয়েন্টার বা shared_ptr-এর মতো উপনামহীন গভীর মান-গ্রাফ। কাঁচা পয়েন্টার থাকা স্ট্রাক্ট কপি করলেও যা দিকে ইশারা করে তা ভাগই থাকে।

const হিসেবে ভাগ করুন। কেবল পড়া ডেটা যেকোনো সংখ্যক থ্রেড থেকে একসঙ্গে পড়া নিরাপদ। কনফিগ মান, মাস্টার ডেটা, গণনার ইনপুট ইত্যাদি নির্মাণের পর আর না লেখা const ভাগ (std::shared_ptr<const Config> যেমন) করলে সিঙ্ক্রোনাইজেশন ছাড়া ভাগ করা যায়। এক সতর্কতা: shared_ptr<const T> যা নিষেধ করে তা কেবল সেই হ্যান্ডেল দিয়ে পরিবর্তন। অন্যত্র নন-const উপনাম বেঁচে থাকলে, বা mutable সদস্য আবার লেখা হলে, প্রতিযোগিতা থাকে — তাই সেটাও ডিজাইন করুন, «নির্মাণ শেষ হলে নন-const রেফারেন্স ছেড়ে দিন আর পরে কেউ না লিখুক» পর্যন্ত। কেবল সিদ্ধান্ত যে «বদল লাগলে জায়গায় বদলানোর বদলে নতুন অবজেক্ট বানিয়ে বদলান» এমন এক পরিবর্তনযোগ্য অবস্থা সরায় যা অন্যথায় পাহারা দিতে হত (স্বয়ং বদলের আয়ু ব্যবস্থাপনার জন্য ধারা ৫.২-এর সতর্কতা দেখুন)।

কিউ দিয়ে হস্তান্তর করুন। থ্রেডগুলোর মধ্যে ডেটা প্রবাহ ভাগ করা ভেরিয়েবলের বদলে প্রডিউসার/কনজিউমার কিউ দিয়ে চালান। C++ স্ট্যান্ডার্ডে চ্যানেল টাইপ নেই, তাই std::mutex + std::condition_variable দিয়ে ছোট কিউ লেখা প্রতিষ্ঠিত প্যাটার্ন।

template <typename T>
class BlockingQueue {
public:
    explicit BlockingQueue(std::size_t capacity) : capacity_(capacity)
    {
        if (capacity == 0)                          // ধারণক্ষমতা ০ সেই ফাঁদ যেখানে প্রতিটি Push চিরকাল অপেক্ষা করে
            throw std::invalid_argument("capacity must be positive");
    }

    // ভর্তি হলে জায়গা (বা থামার অনুরোধ) পর্যন্ত অপেক্ষা। false মানে থামার অনুরোধ।
    bool Push(T item, std::stop_token st)
    {
        {
            std::unique_lock lock(mtx_);
            if (!not_full_.wait(lock, st, [this]{ return queue_.size() < capacity_; }))
                return false;                       // থামার অনুরোধে জাগানো হয়েছে
            if (st.stop_requested())                // জায়গা ও থামা একসঙ্গে হলে থামা অগ্রাধিকার,
                return false;                       // আর থামা শুরু হলে ঠেলা প্রত্যাখ্যান
            queue_.push(std::move(item));
        }
        not_empty_.notify_one();   // লকের বাইরে জানান
        return true;
    }

    // থামার অনুরোধ (stop_token) বা আইটেম আসা পর্যন্ত অপেক্ষা। থামলে nullopt।
    std::optional<T> Pop(std::stop_token st)
    {
        std::optional<T> item;
        {
            std::unique_lock lock(mtx_);
            if (!not_empty_.wait(lock, st, [this]{ return !queue_.empty(); }))
                return std::nullopt;                // থামার অনুরোধে জাগানো হয়েছে
            if (st.stop_requested())                // আইটেম ও থামা একসঙ্গে হলে থামা অগ্রাধিকার,
                return std::nullopt;                // আর থামা শুরু হলে নতুন কাজ শুরু করবেন না
            item = std::move(queue_.front());
            queue_.pop();
        }
        not_full_.notify_one();
        return item;
    }

private:
    const std::size_t capacity_;
    std::mutex mtx_;
    std::condition_variable_any not_empty_;   // condition_variable_any, stop_token-সচেতন wait-এর জন্য
    std::condition_variable_any not_full_;
    std::queue<T> queue_;
};

এখানে দুটি ডিজাইন পয়েন্ট। প্রথম, ধারণক্ষমতা সীমাবদ্ধ করুন আর ভর্তি হলে উৎপাদক পক্ষকে অপেক্ষা করান। উপরের সীমাহীন কিউ সেই বিন্যাসে টাইম বোম হয়ে যায় যেখানে উৎপাদন খরচ ছাড়িয়ে যায়: সে «চলতেই থাকে», কিন্তু মেমোরি বাড়ে। ভর্তি হলে Push ব্লক করা স্বাভাবিক ব্যাকপ্রেশার হিসেবে কাজ করে, ওভারলোড যান্ত্রিকভাবে উপরে পাঠায়। দ্বিতীয়, কন্ডিশন ভেরিয়েবল স্পিউরিয়াস ওয়েকআপের (বিজ্ঞপ্তি ছাড়া জাগা) অধীন, তাই wait সবসময় প্রেডিকেট দিয়ে ডাকুন। wait-এর প্রেডিকেট রূপ ভিতরে «শর্ত সত্য না হওয়া পর্যন্ত লুপ» যুক্তি আপনার হয়ে চালায়।6

5. লক শৃঙ্খলা — RAII ও scoped_lock

ভাগ করা পরিবর্তনযোগ্য অবস্থা কমানোর পরেও প্রায়ই শূন্যে পৌঁছানো যায় না। যা ভাগ থেকে যায় তার জন্য বর্জন ব্যবহার করুন, কিন্তু শৃঙ্খলা ছাড়া লক কেবল প্রতিযোগিতা লুকায়।

প্রথমে, লকের একক «কোডখণ্ড» নয় «ডেটা» ভেবে নিন। রক্ষা করতে চান এমন প্রতিটি পরিবর্তনযোগ্য ডেটা সেটে এক মিউটেক্স দিন (তাকে private সদস্য করুন, বাইরে খুলবেন না), আর সেই ডেটা ছোঁয় এমন প্রতিটি জায়গায় সেই মিউটেক্স নিন — এই সারণির ভাঙা রূপই বেশিরভাগ রেস বাগ আসলে। আর লক ধরে কেবল সেই ডেটা পড়া-লেখাই করা যায়। লক ধরে ফাইল I/O, নেটওয়ার্ক কল ও কলব্যাক (বাহ্যিক কোডে কল) কেবল ধরার সময় বাড়ায় না — পথ খোলে যেখানে ডাকা পক্ষ অন্য লক নিতে গিয়ে ডেডলক করে। লকের বাইরে প্রস্তুত করুন, লকের ভিতরে কেবল বদলান মৌলিক রূপ।

5.1. হাতে lock()/unlock() নিষিদ্ধ

std::mutex-এর lock() / unlock() সরাসরি ডাকা কোড এক্সেপশন বা তাড়াতাড়ি ফিরে আসলে লক ছাড়তে ব্যর্থ হয়। লক নেওয়া ও ছাড়া সবসময় RAII র‍্যাপারের হাতে রাখুন।

র‍্যাপার ব্যবহার
std::lock_guard এক মিউটেক্স ঠিক এক স্কোপ ধরে — সবচেয়ে মৌলিক রূপ
std::scoped_lock (C++17) একাধিক মিউটেক্স একসঙ্গে নেয়। ক্রমের সমস্যা ডেডলক-এড়ানো অ্যালগরিদম দিয়ে সমাধান করে4
std::unique_lock মাঝপথে খুলে আবার বন্ধ করতে চাইলে, বা condition_variable::wait-এ দিতে হলে

দুই বা তার বেশি লক থাকলে থ্রেড অনুসারে নেওয়ার ক্রম উল্টে যাওয়া ক্লাসিক ডেডলক প্যাটার্ন (চিত্র ২-এর চক্রাকার অপেক্ষা ঠিক এভাবে জন্মায়)। সমাধান নিয়ম করা যে «প্রতিটি থ্রেড একই ক্রমে লক নেয়», কিন্তু যখন সেগুলো একই সময়ে নিচ্ছেন, C++-এর ভালো উত্তর আছে: একাধিক মিউটেক্স std::scoped_lock-এ একসঙ্গে দিন আর লাইব্রেরি ডেডলক-মুক্ত নেওয়ার ক্রম গ্যারান্টি করে4 দুই অবজেক্টের মধ্যে স্থানান্তরের মতো «দুটোই লক» চাই পরিস্থিতিতে আলাদা করে কখনো নেবেন না — সবসময় একসঙ্গে নিন।

void Transfer(Account& from, Account& to, int amount)
{
    if (&from == &to) return;                  // একই অ্যাকাউন্টে কিছু করবেন না (নিচের টীকা দেখুন)
    std::scoped_lock lock(from.mtx, to.mtx);   // দুটোই একসঙ্গে; ক্রম লাইব্রেরি সমাধান করে
    from.balance -= amount;
    to.balance   += amount;
}

উপরের পরিচয় পরীক্ষা সাজসজ্জা নয়। একই Account fromto দুটোই হলে আপনি একই অ-পুনরাবৃত্ত মিউটেক্স scoped_lock-এ দুবার দেন, যা হ্যাং বা অনির্ধারিত আচরণ ঘটায়। «দুটোই লক» করে এমন প্রতিটি ফাংশনে একই-অবজেক্ট বর্জন সবসময় যোগ করুন।

«প্রায়ই পড়া, কদাচিৎ লেখা» ডেটার জন্য std::shared_mutex (C++17) পড়া/লেখার লক হিসেবে ব্যবহার করা যায়।12 recursive_mutex এমন টাইপ যা «একই থ্রেড আবার নিলে ভাঙবে না» বলে ডিজাইন, কিন্তু পুনরাবৃত্ত নেওয়া দরকার এমন ডিজাইন প্রায়ই ইঙ্গিত যে লকের দায়িত্বসীমা ঝাপসা হয়ে গেছে — আগে কাঠামো আবার দেখুন।

5.2. atomic-এর সঠিক ভূমিকা

std::atomic এক ভেরিয়েবলে অ্যাটমিক অপারেশন দেয়, প্লাস memory_order-ভিত্তিক ক্রম।5 তার জায়গা .NET সংস্করণের Interlocked-এর মতো পরিস্থিতিতে: এক ভেরিয়েবল আপডেট, যেমন কাউন্টার বা ফ্ল্যাগ। সে একাধিক ভেরিয়েবল একসঙ্গে সামঞ্জস্য রাখতে পারে না, তাই তার জন্য std::mutex-এ ফিরুন।

কাঁচা পয়েন্টার বদলানোর (std::atomic<T*>) নিজস্ব ফাঁদ আছে। বদল নিজে অ্যাটমিক হলেও, পুরনো অবজেক্টের আয়ু বদলানোর পর কেউ রক্ষা করে না। লেখক বদলে delete করার ঠিক আগে পাঠক পুরনো পয়েন্টার লোড করলে মুক্ত মেমোরিতে অ্যাক্সেস হয়। C++-এ «অপরিবর্তনীয় অবজেক্ট বদলে ভাগ» ডিজাইন করতে চাইলে এমন উপায় বেছে নিন যা আয়ু ব্যবস্থাপনার সঙ্গে জোড়া — লক-রক্ষিত std::shared_ptr<const T> বদল, বা C++20-এর std::atomic<std::shared_ptr<T>>

আর বলতে হয়: volatile থ্রেড-সিঙ্ক্রোনাইজেশনের সরঞ্জাম নয়। নিজে memory_order নির্দিষ্ট করা লক-মুক্ত প্রোগ্রামিং বিশেষজ্ঞের এলাকা, যাতে ডিফল্ট (seq_cst) থেকে ঢিলে করার বৈধ কারণ ও সঠিক করার যাচাই দুটোই লাগে। ব্যবসায়িক অ্যাপে হয় ডিফল্ট রাখুন নয়তো শুরু থেকে mutex দিয়ে লিখুন।

6. থামার নকশা — stop_token ও সহযোগী থামা

মাল্টিথ্রেড ডিজাইন পর্যালোচনায় প্রথম প্রশ্ন «এটা কীভাবে থামে?»। আর C++-এ বাইরে থেকে থ্রেড নিরাপদে থামানোর উপায় নেই (Win32-এর TerminateThread কত বিপজ্জনক C সংস্করণ-এ বিস্তারিত)। তাই থ্রেড কীভাবে থামে C++-এর সরঞ্জাম দিয়ে সহযোগী থামার চারপাশে গড়তে হয় — থামানোর পক্ষ কেবল অনুরোধ দেয়; থ্রেড নিজেই ঠিক করে কখন ও কীভাবে শেষ হবে, এমন বিন্দুতে যা জিনিস পরিষ্কার রাখে; আর join সম্পূর্ণ হওয়াই «থেমেছে» গণ্য হয়।

C++20-এ std::jthread-এ থামার ব্যবস্থা তৈরি। request_stop() ডাকলে থ্রেড ফাংশন যে std::stop_token পেয়েছে তাতে থামার অনুরোধ ওঠে, আর লুপ তা পরখ করে। condition_variable_any-এর wait সরাসরি stop_token নিতে পারে, তাই «কাজ আসার অপেক্ষায় থাকা থ্রেড»ও থামার অনুরোধে তৎক্ষণাৎ জাগানো যায় (ধারা ৪-এর BlockingQueue::Pop ঠিক এই রূপ)।

class Worker {
public:
    void Start()
    {
        if (thread_.joinable())                       // ইতিমধ্যে চলতে দ্বৈত Start প্রত্যাখ্যান করুন।
            throw std::logic_error("already running"); // প্রত্যাখ্যানের বদলে অ্যাসাইন করলে নতুন
                                                       // থ্রেড চলতে শুরু করবে, আর পুরনো থামার
                                                       // অপেক্ষা করতে দুই ওয়ার্কার
                                                       // পাশাপাশি চলবে
        thread_ = std::jthread([this](std::stop_token st) {
            try {
                while (!st.stop_requested()) {
                    if (auto item = queue_.Pop(st)) {   // থামার অনুরোধেও জাগে
                        try {
                            Process(*item, st);          // ভিতরে ব্লক করতে পারে এমন কাজেও st দিন
                        } catch (...) {
                            ReportError(std::current_exception());  // এক ব্যর্থতা লিখে চালিয়ে যান
                        }
                    }
                }
            } catch (...) {
                // থ্রেড সীমার শেষ প্রতিরক্ষা রেখা (Pop বা মুভের ব্যর্থতাও ধরে)।
                // এখান থেকে এক্সেপশন বেরোলে std::terminate পুরো প্রসেস ফেলে,
                // তাই ReportError নিজে কখনো ছোড়বে না
                ReportError(std::current_exception());
            }
        });
    }
    // স্পষ্ট Stop লাগে না:
    // Worker-এর ডেস্ট্রাক্টর -> jthread-এর ডেস্ট্রাক্টর -> request_stop() + join()
private:
    BlockingQueue<WorkItem> queue_{100};   // ধারণক্ষমতা সীমাবদ্ধ (ধারা ৪)
    std::jthread thread_;
};
থামার অনুরোধথামানোর পক্ষ- jthread-এর ডেস্ট্রাক্টর, বা request_stopstop_tokenগণনা লুপ:stop_requested() পরখ করেঅপেক্ষমাণ থ্রেড:condition_variable_any::wait(lock, st, pred)তৎক্ষণাৎ জাগেপরিষ্কার করে নিজেই ফেরেjoin rendezvous সম্পূর্ণ করেএখনই থেমেছে বলা যায়

চিত্র 4: C++20 সহযোগী থামা। থামানোর পক্ষ কেবল অনুরোধ দেয়; থ্রেড নিজেই ঠিক করে কীভাবে শেষ হয়; join সম্পূর্ণ হওয়াই থেমেছে গণ্য হয়

আরও এক পয়েন্ট: ওয়ার্কারের ভিতরের try/catch বাদ দেওয়া যায় না। jthread যা এক্সেপশন-নিরাপদ করে তা join, আর কেবল join — থ্রেড ফাংশন থেকে এক্সেপশন বেরোলে std::terminate প্রসেস ফেলে, std::thread-এর মতোই। থ্রেড সীমায় স্পষ্ট ঠিক করুন এক কাজের ব্যর্থতা কীভাবে সামলাবেন (লিখে চালিয়ে যাবেন, নাকি ত্রুটি চ্যানেলে মালিককে জানাবেন)।

একই কারণে খেয়াল করুন Process-এও stop_token দেওয়া হয়। এক কাজের প্রক্রিয়াকরণ ভিতরে ব্লক করলে (নেটওয়ার্ক অপেক্ষা, দীর্ঘ গণনা ইত্যাদি) আর সেই বিন্দু থামার অনুরোধ দেখতে না পারলে, ডেস্ট্রাক্টরের অন্তর্নিহিত join সেই এক আইটেম শেষ হওয়া পর্যন্ত চিরকাল অপেক্ষা করবে। সহযোগী থামা তখনই দাঁড়ায় যখন টোকেন প্রতিটি অপেক্ষার জায়গায় পৌঁছেছে। কাজে বাহ্যিক কল থাকে যা বাধা দেওয়া যায় না, তাহলে টাইমআউট লাগান আর এক আইটেম কতক্ষণ চলতে পারে তার উপরের সীমা দিন।

C++17-এর আগের পরিবেশে একই রূপ std::atomic<bool> থামার ফ্ল্যাগ প্লাস condition_variable-এর notify_all দিয়ে হাতে গড়েন। এখানে মূল কথা থামার-ফ্ল্যাগ পরীক্ষাকে কন্ডিশন ভেরিয়েবলের প্রেডিকেটে ভাঁজ করা — কেবল ফ্ল্যাগ তুলে বিজ্ঞপ্তি ভুলে গেলে অপেক্ষমাণ থ্রেড কখনো জাগবে না।

7. Windows-নির্দিষ্ট বিষয় — Win32 API-এর সীমানা

7.1. স্ট্যান্ডার্ড লাইব্রেরি ও Win32 সিঙ্ক্রোনাইজেশন অবজেক্টের মধ্যে বেছে নেওয়া

Microsoft ডকুমেন্টেশন পোর্টেবিলিটি-অগ্রাধিকার C++ কোডে std::mutex / std::shared_mutex সুপারিশ করে, আর Win32 সিঙ্ক্রোনাইজেশন অবজেক্টের জায়গা «Win32 অপেক্ষা API লাগলে» ও «ক্রস-প্রসেস সিঙ্ক্রোনাইজেশন» বলে সাজায়।8

পরিস্থিতি পছন্দ
সাধারণ ইন্ট্রা-প্রসেস বর্জন std::mutex + RAII (ডিফল্ট)
অনেক পড়া, কম লেখা std::shared_mutex
WaitForMultipleObjects দিয়ে একাধিক অবজেক্ট একসঙ্গে অপেক্ষা ইভেন্ট ও মিউটেক্সের মতো Win32 কার্নেল অবজেক্ট
ক্রস-প্রসেস বর্জন / বিজ্ঞপ্তি নামযুক্ত মিউটেক্স, ইভেন্ট, সেমফোর
Win32 API সরাসরি ব্যবহার করে ইন্ট্রা-প্রসেস লক SRW লক (পুনরাবৃত্তি লাগলেই CRITICAL_SECTION)8

প্রসেস পেরিয়ে শেয়ার্ড মেমোরি অ্যাক্সেস বর্জনের কংক্রিট ডিজাইনের জন্য দেখুন «শেয়ার্ড মেমোরির ফাঁদ ও ব্যবহারিক সেরা অনুশীলন»।

7.2. DllMain-এর ভিতরে থ্রেড ছুঁবেন না

DLL লেখার সময় গুরুতর বাধা লোডার লকDllMain লোডার লক ধরে ডাকা হয়, তাই তার ভিতরে অন্য থ্রেডের সঙ্গে সিঙ্ক্রোনাইজ করা, থ্রেড শেষ হওয়ার অপেক্ষা, বা LoadLibrary ডাকা ডেডলক বা অপ্রত্যাশিত আচরণ ঘটায়। থ্রেড শুরু বা join করা যেকোনো ইনিশিয়ালাইজেশন DllMain-এর বাইরে, স্পষ্ট ইনিশিয়ালাইজেশন ফাংশনে নিয়ে যান।13

7.3. UI থ্রেড ও COM অ্যাপার্টমেন্ট

Windows ডেস্কটপ অ্যাপে ভাষা নির্বিশেষে প্রযোজ্য শক্ত বাধা আছে: কেবল যে থ্রেড উইন্ডো বা কন্ট্রোল তৈরি করেছে — UI থ্রেড — সেই ছুঁতে পারে। Windows উইন্ডো মেসেজ সেই থ্রেডের মেসেজ কিউতে পৌঁছায় যে থ্রেড সেই উইন্ডো তৈরি করেছে, তাই UI তৈরি ও পরিচালনা সেই থ্রেডে কেন্দ্রীভূত থাকতে হয়। ওয়ার্কার থ্রেড থেকে স্ক্রিন আপডেট করতে চাইলে সরাসরি ছুঁবেন না — PostMessage (অ্যাসিঙ্ক) দিয়ে UI থ্রেডকে বলুন, আর UI থ্রেডের উইন্ডো প্রসিজারে সামলান। সিঙ্ক রূপ SendMessage ডাকা, যখন UI থ্রেড সেই ওয়ার্কার শেষ হওয়ার অপেক্ষা করছে, ডেডলক ঘটায় যেখানে প্রত্যেকে অন্যের অপেক্ষা করে, তাই ওয়ার্কারের বিজ্ঞপ্তির জন্য অ্যাসিঙ্ক রূপকে ডিফল্ট করুন। STA/MTA, যেখানে COM জড়িত, «COM STA/MTA মূলকথা - থ্রেডিং মডেল ও হ্যাং কীভাবে এড়াবেন»-এ আছে। আরও খেয়াল রাখুন /clr দিয়ে কম্পাইল করা C++/CLI কোডে <thread><mutex>-এর মতো স্ট্যান্ডার্ড থ্রেড হেডার ব্লকড।14

8. যাচাই ও ডিবাগ — এই অনুমানে প্রস্তুতি যে পুনরুৎপাদন হবে না

রেস বাগ খুঁজতে টেস্টের ওপর ভরসা করা যায় না, কারণ সাধারণ টেস্ট «ঘটনাক্রমে রেস হয়নি» চালনাকে পাস গণে। প্রস্তুতি তিন স্তরে ভাবুন।

প্রথম প্রতিরক্ষা রেখা এখন পর্যন্ত আলোচিত ডিজাইন নীতিগুলো, ঠিক যেমন আছে। রিভিউতে সারণি দিয়ে নিশ্চিত করুন: কোন পরিবর্তনযোগ্য ডেটা ভাগ, কোন মিউটেক্স কোন টুকরো রক্ষা করে, একাধিক লক নেওয়ার ক্রম অনন্য কি না (বা scoped_lock দিয়ে একসঙ্গে নেওয়া), আর থামার পথ কোথায়। যে ডিজাইনের জন্য এই সারণি লিখতে পারেন না সেটা শেষ নয়, এখন যত ভালোই চলুক।

দ্বিতীয়, অস্বাভাবিক অবস্থা লুকানোর বদলে পর্যবেক্ষণযোগ্য করুন। যে লক কখনো নিতে ব্যর্থ হওয়া উচিত নয় তাতে timed_mutex-এর try_lock_for বা condition_variable-এর wait_for দিয়ে টাইমআউট লাগান, আর টাইমআউট অস্বাভাবিক হিসেবে লগ করুন — সেটা চিরকালের হ্যাংকে শনাক্তযোগ্য ব্যর্থতায় বদলায়। থ্রেড সীমার try/catch (ধারা ৬)-এ ধরা এক্সেপশন সবসময় লগ করুন। মাঠে হ্যাং বা ক্র্যাশ হলে ডাম্প নিন, প্রতিটি থ্রেডের স্ট্যাক দেখুন, আর দেখুন তাদের লক অপেক্ষা চক্র গড়ছে কি না। ডাম্প ও লগিং সাজানো «Windows অ্যাপ ক্র্যাশে লগ ও ডাম্প রাখার ডিজাইন»-এ আছে।

তৃতীয়, লোডের নিচে ঝাঁকান। কোরের চেয়ে বেশি সমান্তরালতায় দীর্ঘক্ষণ চালানো, প্রক্রিয়াকরণ ক্রম এলোমেলো করা, আর কৃত্রিম বিলম্ব ঢোকানো ব্যবহারিক স্ট্রেস-টেস্ট কৌশল যা ডেভেলপমেন্ট মেশিনে রেসের «জ্যাকপট» লাগানো সহজ করে। ডিবাগ বিল্ডে মিলিয়ে যাওয়া বাগ প্রায়ই অপ্টিমাইজড রিলিজ বিল্ডে ভারী লোডে সহজে পুনরুৎপাদিত হয়।

9. সারাংশ — C++ চেকলিস্ট

প্রতিটি ভাষার সাধারণ নীতির ওপর C++-নির্দিষ্ট পরীক্ষা চাপান: থ্রেড সরাসরি তৈরি করবেন না, ভাগ করা পরিবর্তনযোগ্য অবস্থা ন্যূনতম করুন, লক ও ডেটার এক-এক মিল, সহযোগী থামা।

  1. std::thread খালি ব্যবহার হচ্ছে কি (jthread হতে পারে? এক্সেপশন পথেও join নিশ্চিত?)
  2. detach() ব্যবহার হচ্ছে না তো?
  3. ল্যাম্বডা ক্যাপচার স্পষ্ট কি, আর রেফারেন্স-ক্যাপচার করা ভেরিয়েবল থ্রেডের চেয়ে বেশি বাঁচে?
  4. বিশ্বাস করে বলতে পারেন কোথাও একটিও অসিঙ্ক্রোনাইজড ভাগ করা পরিবর্তনযোগ্য অ্যাক্সেস (= অনির্ধারিত আচরণ) নেই?
  5. হাতে lock() / unlock() নেই, আর একাধিক লক scoped_lock দিয়ে একসঙ্গে নেওয়া হয়?
  6. প্রতিটি condition_variable::wait প্রেডিকেটসহ?
  7. ভাগ করা ফ্ল্যাগে volatile ব্যবহার হচ্ছে না (std::atomic তার জায়গায়)?
  8. থামার পথ stop_token (বা অ্যাটমিক ফ্ল্যাগ প্লাস বিজ্ঞপ্তি) ঘিরে ডিজাইন, join সম্পূর্ণ হওয়া rendezvous নিশ্চিত করে?
  9. std::async-এর future ফেলা হচ্ছে না?
  10. DllMain থ্রেড শুরু, সিঙ্ক্রোনাইজ বা join থেকে মুক্ত?

মাল্টিথ্রেডেড C++ অনির্ধারিত আচরণের কিনারার ঠিক পাশে হাঁটার কাজ, কিন্তু উল্টে বললে RAII ও স্ট্যান্ডার্ড লাইব্রেরির রীতিতে সততার সঙ্গে চলা সেই কিনারা থেকে আসল দূরত্ব রাখে। jthread, scoped_lock, প্রেডিকেট-রূপ wait, atomic — এই সরঞ্জামে সঠিক ডিফল্ট বেছে নেওয়াই, C++-এ, ডিজাইন নীতির অনুশীলন।

সম্পর্কিত নিবন্ধ

সম্পর্কিত পরামর্শ ক্ষেত্র

KomuraSoft LLC C++ অ্যাপ ও DLL-এর মাল্টিথ্রেড ডিজাইন রিভিউ, «মাঝে মাঝে ক্র্যাশ» বা «কেবল রিলিজ বিল্ডে অদ্ভুত আচরণ»-এর মতো রেস-কন্ডিশন বাগের মূল কারণ তদন্ত (ডাম্প বিশ্লেষণ), এবং পুরনো থ্রেডেড কোড আধুনিক C++-এ মাইগ্রেশনের পরামর্শ সামলায়।

তথ্যসূত্র

  1. ISO C++, C++ Core Guidelines - CP: Concurrency and parallelism। CP.1 (ধরে নিন আপনার কোড মাল্টিথ্রেডেড প্রোগ্রামের অংশ হিসেবে চলবে) ও CP.2 (ডেটা রেস এড়ান) কনকারেন্সি ও প্যারালালিজম অধ্যায়ের উদ্বোধনী নিয়ম; ডেটা রেস থাকলে কোনো গ্যারান্টিই টেকে না; আর সমবর্তী কোডের ডিজাইন নিয়ম — লক কতদূর ধরা হয়, RAII ব্যবহার ইত্যাদি — সেখানে ব্যবস্থাবদ্ধ।  2

  2. cppreference.com, std::jthread। C++20-এর jthread std::thread থেকে এই কারণে আলাদা যে তার ডেস্ট্রাক্টর স্বয়ংক্রিয়ভাবে request_stop() ডেকে তারপর join করে; থ্রেড ফাংশনের অগ্র তর্ক হিসেবে std::stop_token নিতে পারে; আর এতে এক্সেপশন ছোড়া হলেও join ও থামার অনুরোধ দুটোই নিশ্চিত হয়।  2

  3. Microsoft Learn, Microsoft C/C++ language conformance by Visual Studio version। P0660R10 (<stop_token> ও jthread) এবং P1135R6 (C++20 সিঙ্ক্রোনাইজেশন লাইব্রেরি) Visual Studio 2019 16.9 থেকে সমর্থিত; আর C++ স্ট্যান্ডার্ড লাইব্রেরি বৈশিষ্ট্যের সংস্করণভিত্তিক সমর্থন অবস্থা।  2 3

  4. Microsoft Learn, scoped_lock Class। C++17-এর scoped_lock নির্মাণে এক বা একাধিক মিউটেক্স নেয় ও ডেস্ট্রাক্টরে ছাড়ে; একাধিক মিউটেক্স একসঙ্গে দিলে std::lock-সমতুল্য ডেডলক-এড়ানো অ্যালগরিদম দিয়ে নেওয়া হয়; এক্সেপশন ছোড়া হলেও নির্ভরযোগ্যভাবে ছাড়ে; আর এক মিউটেক্স হলে lock_guard/unique_lockও বিকল্প।  2 3

  5. Microsoft Learn, <atomic>। অ্যাটমিক অপারেশন অবিভাজ্য, তাই অন্য থ্রেড কেবল অপারেশনের আগে বা পরে অবস্থা দেখতে পারে; memory_order তর্কের ভিত্তিতে অন্য অ্যাটমিক অপারেশনের দৃশ্যমানতায় ক্রম প্রয়োজনীয়তা স্থাপন ও সেগুলো ভঙ্গ করা কম্পাইলার অপ্টিমাইজেশন দমন; atomic_flag সবসময় লক-মুক্ত; আর /clr:pure-এ এই হেডার ব্লকড।  2

  6. Microsoft Learn, <condition_variable>। কন্ডিশন ভেরিয়েবলে অপেক্ষায় মিউটেক্স লাগে, অপেক্ষার সময় লক ছাড়া থাকে; স্পিউরিয়াস ওয়েকআপ আছে — বিজ্ঞপ্তি ছাড়া জাগা — তাই অপেক্ষার পক্ষ ফিরে এসে শর্ত স্পষ্টভাবে আবার দেখুক, আর প্রেডিকেট রূপ wait(lock, pred) সেই লুপ আপনার হয়ে চালায়; আর condition_variable_any যেকোনো মিউটেক্স টাইপের সঙ্গে জোড়া যায়।  2

  7. Microsoft Learn, <future>। future ও shared_future-এর ডেস্ট্রাক্টর নিয়মত ব্লক করে না, একমাত্র ব্যতিক্রম std::async দিয়ে চালু কাজের সঙ্গে বাঁধা future (বা শেষ shared_future) কাজ অসম্পূর্ণ থাকতে ডেস্ট্রাক্টর চললে শেয়ার্ড স্টেট ready না হওয়া পর্যন্ত ব্লক করে — আচরণ স্ট্যান্ডার্ডে স্পষ্ট নোট।  2

  8. Microsoft Learn, About Synchronization। Win32 সিঙ্ক্রোনাইজেশন প্রিমিটিভ বেছে নেওয়ার নির্দেশনা: পোর্টেবিলিটি-অগ্রাধিকার C++ কোডে std::mutex / std::shared_mutex ও RAII সুপারিশকৃত; Win32 অপেক্ষা API বা ক্রস-প্রসেস সিঙ্ক্রোনাইজেশন লাগলে Win32 সিঙ্ক্রোনাইজেশন অবজেক্ট; নতুন ইন্ট্রা-প্রসেস কোডের ডিফল্ট SRW লক, CRITICAL_SECTION পুনরাবৃত্ত নেওয়ার জন্য সংরক্ষিত; আর ইন্ট্রা-প্রসেস সিঙ্ক্রোনাইজেশনে Mutex ব্যবহার «সাধারণ ভুল» কারণ সবসময় কার্নেল ট্রানজিশন হয়।  2 3

  9. cppreference.com, std::thread::~thread। std::thread-এর ডেস্ট্রাক্টর std::terminate ডাকে যদি থ্রেড এখনও joinable থাকতে (joinও হয়নি detachও হয়নি) ডাকা হয় — অর্থাৎ থ্রেড অবজেক্ট ধ্বংসের আগে join বা detach-এর সিদ্ধান্ত ব্যতিক্রমহীনভাবে শেষ হতে হবে। 

  10. Microsoft Learn, Best Practices in the Parallel Patterns Library। সমান্তরালতা আদর্শত যত উঁচু স্তরে সম্ভব (বাইরের লুপ) প্রকাশ করা উচিত; প্রতি ইটারেশনের কাজ ছোট বা অসম যে সমান্তরাল লুপে fork/join শিডিউলিং ওভারহেড সমান্তরাল এক্সিকিউশনের লাভ ছাড়িয়ে যেতে পারে; আর প্রসেসর সংখ্যা বাড়লে সেই প্রবণতা শক্ত হয়। 

  11. Microsoft Learn, Microsoft C/C++ language conformance by Visual Studio version। C++17 সমান্তরাল অ্যালগরিদম লাইব্রেরি সম্পূর্ণ, অথচ «সম্পূর্ণ» মানে প্রতিটি অ্যালগরিদম প্রতিটি ক্ষেত্রে সমান্তরাল নয়; সবচেয়ে গুরুত্বপূর্ণ অ্যালগরিদম সমান্তরাল করে যেগুলো নয় তাদের জন্যও এক্সিকিউশন-পলিসি স্বাক্ষর দেওয়ার ইমপ্লিমেন্টেশন নীতি। 

  12. Microsoft Learn, C++ standard library header files। মাল্টিথ্রেডিং-সম্পর্কিত স্ট্যান্ডার্ড হেডার সাজানো <atomic> (C++11), <mutex> (C++11), <shared_mutex> (C++14), <condition_variable> (C++11), <future> (C++11), <stop_token> / <semaphore> / <latch> / <barrier> (C++20), ও <thread> (C++11) হিসেবে। 

  13. Microsoft Learn, Dynamic-Link Library Best Practices। DllMain লোডার লক ধরে ডাকা হয়, কোন API ডাকা যায় তাতে গুরুতর বাধা; DllMain-এর ভিতরে অন্য থ্রেডের সঙ্গে সিঙ্ক্রোনাইজ করা ডেডলক করতে পারে; LoadLibrary ডাকা বা থ্রেড শেষ হওয়ার অপেক্ষা সাধারণ নিষিদ্ধ কাজ; ইনিশিয়ালাইজেশন যত দূরে সম্ভব বিলম্বিত করে DllMain-এর বাইরে নেওয়া উচিত; আর লোডার লককে শীর্ষে রেখে লক শ্রেণিবিন্যাস সংজ্ঞায়িত করা উচিত। 

  14. Microsoft Learn, <thread>। <thread> হেডার thread ক্লাস ও sleep_for-এর মতো সহায়ক ফাংশন সংজ্ঞায়িত করে; /clr দিয়ে কম্পাইল কোডে এই হেডার ব্লকড; আর STDCPP_THREADS ম্যাক্রো দিয়ে থ্রেড সমর্থন আছে কি না জানা যায়। 

কাছাকাছি বিষয়ে গভীরে যেতে একই ট্যাগযুক্ত সাম্প্রতিক নিবন্ধ।

ব্যবহারিক মাল্টিথ্রেডিং সেরা অনুশীলন: C সংস্করণ — Win32 API পথে নিরাপদে লেখা

Win32-এর সাথে C-তে মাল্টিথ্রেডিংয়ের স্থিত পথ হল _beginthreadex দিয়ে থ্রেড তৈরি, SRW লক ও কন্ডিশন ভেরিয়েবল, Interlocked ফাংশন, এবং স্টপ...

DllMain ও লোডার লক — DLL ইনিশিয়ালাইজেশনে "কিছুই করবেন না" বলা হয় তার আসল কারণ

DllMain থেকে LoadLibrary ডাকা বা অন্য থ্রেডের সাথে সিঙ্ক্রোনাইজ করা যায় না কেন। প্রাথমিক উৎস থেকে এই নিবন্ধ ব্যাখ্যা করে লোডার লক প্রতিট...

Spurious Wakeup — কন্ডিশন ভেরিয়েবল কেন "নোটিফাই না হয়েও" জাগে এবং Windows-এ সঠিকভাবে কীভাবে অপেক্ষা করবেন

কন্ডিশন ভেরিয়েবলের ওয়েট নোটিফিকেশন না এলেও ফিরতে পারে (spurious wakeup)। এই নিবন্ধ Windows বাস্তবায়ন থেকে ব্যাখ্যা করে স্পেসিফিকেশন কে...

ব্যবহারিক মাল্টিথ্রেডিং বেস্ট প্র্যাকটিস: Java সংস্করণ — ভার্চুয়াল থ্রেড যুগের রীতি

Java-তে মাল্টিথ্রেডিংয়ের প্রতিষ্ঠিত রীতি হলো থ্রেড সরাসরি না তৈরি করে ExecutorService ও ভার্চুয়াল থ্রেডে চড়া। এই নিবন্ধ ব্যবহারিক নীতি...

এই পৃষ্ঠাগুলো বিষয়টিকে সেবা ও সিদ্ধান্তের বৃহত্তর প্রেক্ষাপটে স্থাপন করে।

নিবন্ধটি নিচের সেবাগুলোর সাথে সরাসরি সম্পর্কিত।

প্রায়শ জিজ্ঞাসিত প্রশ্ন

এই নিবন্ধের বিষয়ে পরামর্শে প্রায়ই আসা প্রশ্ন।

std::mutex আর Win32-এর CRITICAL_SECTION / SRW লক কীভাবে বেছে নেব?
পোর্টেবিলিটিকে অগ্রাধিকার দেওয়া সাধারণ C++ কোডে std::mutex / std::shared_mutex সাথে RAII র‍্যাপার (lock_guard / scoped_lock) প্রথম পছন্দ। Win32 সিঙ্ক্রোনাইজেশন অবজেক্ট তখন নিন যখন WaitForMultipleObjects-এর মতো Win32 অপেক্ষা API-এর সঙ্গে জোড়া লাগাতে হয়, বা নামযুক্ত অবজেক্ট দিয়ে ক্রস-প্রসেস সিঙ্ক্রোনাইজেশন লাগে। প্রসেসের ভিতরে Win32 API সরাসরি ব্যবহার করলে নতুন কোডের ডিফল্ট SRW লক, আর CRITICAL_SECTION কেবল তখনই যখন একই থ্রেডকে পুনরাবৃত্তভাবে নিতে হয়। ইন্ট্রা-প্রসেস বর্জনের জন্য Win32 Mutex ব্যবহার করা ক্লাসিক ভুল, কারণ তাতে সবসময় কার্নেল ট্রানজিশন হয় এবং সেই অনুপাতে ধীর।
std::thread-এর detach() ব্যবহার করা যায়?
নিয়ম হিসেবে এড়িয়ে চলুন। detached থ্রেড join-এর সব উপায় হারায়, আর প্রসেস বেরোনোর সময় সে এখনও চলছে কি না তা নিয়ন্ত্রণ যায়। ক্লাসিক দুর্ঘটনা: detached থ্রেড স্ট্যাটিক ভেরিয়েবল বা হিপ ধ্বংসের পরেও চলতে থাকে এবং শাটডাউন ক্র্যাশ ঘটায়। থ্রেড শেষ হওয়া পর্যন্ত অপেক্ষা করতে পারা থ্রেড ডিজাইনের মৌলিক শর্ত, তাই jthread (যা স্বয়ংক্রিয় join করে) ব্যবহার করুন, অথবা thread হলে স্কোপ শেষ হওয়ার আগে সবসময় join করার কাঠামো দিন। detach কেবল সেই সংকীর্ণ পরিস্থিতিতে গ্রহণযোগ্য যেখানে থ্রেড প্রসেসের ভাগ্য ভাগ করতে পারে এবং আপনি নিশ্চিত করতে পারেন সে ভাগ করা অবস্থায় একেবারেই ছোঁয় না।
C++-এ থ্রেডগুলোর মধ্যে সিঙ্ক্রোনাইজেশনের জন্য volatile ব্যবহার করা যায়?
না। C++-এর volatile এমন পড়া-লেখার জন্য কোয়ালিফায়ার যা আপনি কম্পাইলারকে অপ্টিমাইজ করে সরিয়ে দিতে চান না — যেমন মেমোরি-ম্যাপড I/O — এবং তা থ্রেডগুলোর মধ্যে দৃশ্যমানতা বা ক্রম গ্যারান্টি দেয় না। একাধিক থ্রেড সিঙ্ক্রোনাইজেশন ছাড়া একই ভেরিয়েবলে গেলে সেটা ডেটা রেস, আর সেটা অনির্ধারিত আচরণ। থ্রেডগুলোর মধ্যে ভাগ করা ফ্ল্যাগ ও কাউন্টারের জন্য std::atomic ব্যবহার করুন, আর একাধিক ভেরিয়েবল একসঙ্গে রক্ষা করতে std::mutex। std::atomic অপারেশনের অবিভাজ্যতা এবং memory_order-ভিত্তিক ক্রম দুটোই দেয়।
std::async সুবিধাজনক দেখায়, কিন্তু ফাঁদ আছে?
সবচেয়ে বড় ফাঁদ future-এর ডেস্ট্রাক্টর। std::async দিয়ে চালু কাজের সঙ্গে বাঁধা future (বা শেষ shared_future) কাজ অসম্পূর্ণ থাকতে ডেস্ট্রাক্টর চললে সম্পূর্ণ পর্যন্ত ব্লক করে। ফেরত future ধরে না রেখে ফেলে দিলে সেটা সেখানেই সিঙ্ক্রোনাস এক্সিকিউশনের সমান হয়ে যায় — দুর্ঘটনা যেখানে অ্যাসিঙ্ক যেতে চেয়েছিলেন কিন্তু সিরিয়াল হয়ে গেলেন। তাছাড়া লঞ্চ পলিসি না দিলে কাজ সত্যি আলাদা থ্রেডে চলে কি না ইমপ্লিমেন্টেশনের বিবেচনায়। ব্যবহার করলে future-এর আয়ু স্পষ্টভাবে সামলান, আর যেখানে সমবর্তী এক্সিকিউশন নিশ্চিত করতে হয় সেখানে std::launch::async নির্দিষ্ট করুন।

লেখকের প্রোফাইল

নিবন্ধের লেখকের পরিচিতি পৃষ্ঠা।

Go Komura

KomuraSoft LLC-এর প্রতিনিধি

Windows সফটওয়্যার ডেভেলপমেন্ট, প্রযুক্তিগত পরামর্শ ও বাগ তদন্তে বিশেষজ্ঞ, বিশেষ করে বিদ্যমান সিস্টেমযুক্ত প্রকল্প ও পুনরুৎপাদন করা কঠিন বাগে।

পাবলিক লিঙ্ক

ব্লগে ফিরে যান