সরাসরি পর্বে যান
পর্ব ৪৬ · ৫৫-এর মধ্যে · মডিউল ৭

async, future আর atomics — পেছনে কাজ, পরে ফল

async, future and atomics — Background Work, Results Later

Taught in Bangla, this lesson moves from threads to tasks: std::async and std::future, launch policies and exceptions through get(), the discarded-future trap, promise, packaged_task and shared_future, std::atomic and compare_exchange, acquire/release memory ordering, and parallel algorithms with C++26 std::execution.

এই পর্বে যা শিখবেন

  • thread সামলানোর বদলে “কাজ দাও, পরে ফল নাও” — task-ভিত্তিক চিন্তা; std::async-এর launch policy আর future-এর get(), wait_for
  • পেছনের কাজের exception কীভাবে future দিয়ে ফিরে আসে, আর ফেলে দেওয়া future কেন কাজকে ক্রমিক বানিয়ে দেয়
  • std::promise, std::packaged_task আর std::shared_future — হাতে হাতে ফল পাঠানো আর অনেক পাঠককে একই ফল দেওয়া
  • std::atomic: fetch_add, compare-exchange লুপ, is_lock_free, atomic_flag; কখন atomic, কখন mutex
  • memory ordering-এর অন্তর্দৃষ্টি (seq_cst, acquire/release, relaxed), parallel algorithm আর C++26-এর std::execution; আর AI-এর লেখা concurrent কোড যাচাই

১ · কর্মী নয়, ফল চাই — task-ভিত্তিক চিন্তা

আগের পর্বে আপনি std::thread দিয়ে নতুন thread চালিয়েছেন, join করেছেন, আর ভাগ করা ডেটা mutex দিয়ে পাহারা দিয়েছেন। সেখানে আপনি ছিলেন একজন ম্যানেজার: কর্মী নিয়োগ করা, তাকে কাজ বুঝিয়ে দেওয়া, শেষে তার জন্য অপেক্ষা করা, আর কাজের ফল কোথায় রাখবে তা আগে থেকে ঠিক করে দেওয়া। একটা সমস্যাও দেখেছেন: std::thread কোনো মান ফেরত দেয় না। ফল চাইলে একটি ভাগ করা ভ্যারিয়েবল বানাতে হয়, তাকে mutex দিয়ে ঘিরতে হয়, আর join-এর পরে সেটা পড়তে হয়।

কিন্তু অনেক সময় আপনি কর্মী সামলাতে চান না। আপনি শুধু বলতে চান: “এই হিসাবটা পেছনে করে রাখো, আমি পরে উত্তর নিয়ে যাব।” কে করবে, কোন thread-এ করবে, ফল কোথায় রাখবে — এসব নিয়ে মাথা ঘামাতে চান না। এই চিন্তাকে বলে task-ভিত্তিক প্রোগ্রামিং: আপনি ভাবেন কাজ আর তার ফল নিয়ে, কর্মী নিয়ে নয়।

ঈদের আগে দর্জির দোকানে পাঞ্জাবি বানাতে দিয়েছেন। কাপড় আর মাপ দিলেন, দর্জি একটি রসিদ ধরিয়ে দিলেন। আপনি দোকানে বসে থাকেন না — বাসায় ফিরে অন্য কাজ করেন। কোন কারিগর সেলাই করবেন, কোন মেশিনে, তা আপনি জানেনও না। নির্দিষ্ট দিনে রসিদ নিয়ে গেলে পাঞ্জাবি হাতে পান। পাঞ্জাবি তৈরি না থাকলে কাউন্টারে দাঁড়িয়ে অপেক্ষা করেন। আর যদি কাপড় কাটতে গিয়ে নষ্ট হয়ে যায়? দর্জি সেটাও রসিদের বিপরীতেই জানান — “আপনার কাপড়ে সমস্যা হয়েছে।” খবরটা হারিয়ে যায় না। C++-এ এই রসিদের নাম std::future.

এই পর্বের দ্বিতীয় ভাগ আরও নিচের স্তরে। একটি website-এর hit counter ভাবুন: হাজারো request একসাথে এসে শুধু একটি সংখ্যা এক করে বাড়াতে চায়। এর জন্য পুরো mutex লাগানো অনেকটা একটি টাকা জমা দিতে ব্যাংকের পুরো শাখা বন্ধ করার মতো। CPU-তে কিছু ছোট নির্দেশ আছে, যা মাঝপথে কেউ ভাঙতে পারে না — পড়া, বদলানো আর লেখা এক ধাক্কায় হয়। C++ এগুলো দেয় std::atomic নামে। এগুলো দিয়ে তালা ছাড়াই নিরাপদ counter বানানো যায়, আর এক thread থেকে আরেক thread-কে “ডেটা তৈরি” সংকেত দেওয়া যায়।

thread (পর্ব ৪৫)

আপনি কর্মী সামলান: চালানো, join, ফল রাখার জায়গা, তালা। পূর্ণ নিয়ন্ত্রণ, বেশি দায়িত্ব।

worker

async + future

আপনি কাজ দেন, রসিদ নেন। ফল আর exception রসিদ দিয়েই ফেরে। ভাগ করা ভ্যারিয়েবল লাগে না।

task

atomic

একটি মাত্র ভ্যারিয়েবলে ছোট, অবিভাজ্য কাজ — counter, পতাকা, সর্বোচ্চ মান। তালার দরকার নেই।

indivisible

২ · std::async আর std::future — পেছনে হিসাব, পরে উত্তর

একটি ভ্রমণ অ্যাপ ভাবুন। স্ক্রিন খোলার সময় তাকে ডলারের বিনিময় হার আর আবহাওয়ার পূর্বাভাস — দুটোই আনতে হয়। আসল অ্যাপে এগুলো নেটওয়ার্ক থেকে আসত; আমরা ধরে নিই এগুলো সময়সাপেক্ষ হিসাব। পুরোনো পথে একটির পর একটি হিসাব করলে দুটির সময় যোগ হয়। <future> header-এর std::async দিয়ে দুটোই একসাথে পেছনে শুরু করা যায়:

#include <future>

double fetchRate();        // সময়সাপেক্ষ হিসাব (উদাহরণ)
int    fetchWeather();

auto rate    = std::async(std::launch::async, fetchRate);     // রসিদ ১
auto weather = std::async(std::launch::async, fetchWeather);  // রসিদ ২
drawScreen();                                // এর মধ্যে নিজের কাজ
std::println("USD {} | {} C", rate.get(), weather.get());

std::async একটি ফাংশন (বা lambda) আর তার argument নেয়, কাজটা চালানোর ব্যবস্থা করে, আর সাথে সাথে ফেরত দেয় একটি std::future<T> — যেখানে T হলো ফাংশনের return টাইপ। future আর পেছনের কাজের মাঝে একটি লুকানো জায়গা থাকে, যার নাম shared state. কাজ শেষ হলে ফল (বা exception) সেখানে রাখা হয়; future সেখান থেকে তুলে আনে। এ জন্যই কোনো ভাগ করা ভ্যারিয়েবল বা mutex আপনাকে লিখতে হয়নি।

Launch policy — কখন, কোথায় চলবে

প্রথম argument-টি হলো launch policy. এখানে সতর্কভাবে পড়ুন, কারণ অনেক বই ও AI এটি ভুল বলে:

policyকী হয়কখন ব্যবহার
std::launch::asyncকাজটি এমনভাবে চলে, যেন একটি নতুন thread-এ চলছে — ডাকার সাথে সাথে, সমান্তরালেযখন সত্যিই একসাথে চলা দরকার
std::launch::deferredএখন কিছুই চলে না। প্রথমবার get() বা wait() ডাকলে কাজটি ডাকার thread-এই চলে। কেউ না ডাকলে কখনোই চলে নাঅলস হিসাব — দরকার হলে তবেই করা
policy না দিলেমানে async | deferred — দুটোর যেকোনোটি, লাইব্রেরি নিজে বেছে নেয়. সমান্তরালে চলবে, এমন কোনো নিশ্চয়তা নেইযখন সত্যিই কিছু যায় আসে না (বিরল)

তৃতীয় সারিটিই ফাঁদ। policy না দিলে কাজটি deferred হতেই পারে — তখন সেটা আদৌ পেছনে চলে না, আপনি get() ডাকলে আপনার নিজের thread-এই চলে। বাস্তবে অনেক লাইব্রেরি সুযোগ থাকলে নতুন thread নেয়, কিন্তু standard তা বাধ্য করে না। তাই নিয়ম সহজ: সমান্তরালে চলা দরকার হলে সবসময় std::launch::async লিখুন।

get(), wait() আর wait_for()

  • get() — ফল তৈরি না হওয়া পর্যন্ত ডাকার thread-কে আটকে রাখে (block), তারপর ফল ফেরত দেয়। একবারই ডাকা যায়। ডাকার পর future-এর valid() হয়ে যায় false; আবার get() ডাকা undefined behavior (অনেক লাইব্রেরি তখন std::future_error ছোড়ে, কিন্তু তার ওপর ভরসা করবেন না)। কারণ get() ফলটি move করে বের করে আনে — রসিদ জমা দিলে পাঞ্জাবি আপনার, রসিদ আর থাকে না।
  • wait() — শুধু অপেক্ষা করে, ফল তোলে না। পরে get() ডাকা যায়।
  • wait_for(সময়) — বড়জোর ততক্ষণ অপেক্ষা করে, তারপর অবস্থা জানায়: ready (তৈরি), timeout (এখনো হয়নি) বা deferred (কাজটি deferred, এখনো শুরুই হয়নি)। এতে UI thread আটকে না থেকে মাঝে মাঝে খোঁজ নিতে পারে: “তৈরি হলে দেখাও, না হলে ঘুরন্ত চিহ্ন দেখাতে থাকো।”

পুরো জীবনচক্রটা ধাপে ধাপে দেখুন — শেষ ধাপে exception-এর পথও আছে। প্রতিটি ধাপে আগে নিজে অনুমান করুন, তারপর “পরে” চাপুন:

1double fetchRate() { /* হিসাব */ return 117.5; }2auto f = std::async(std::launch::async, fetchRate);3drawScreen();               // main নিজের কাজ করছে4double usd = f.get();       // ফল না আসা পর্যন্ত অপেক্ষা5std::println("USD = {}", usd);6// fetchRate() throw করলে → get() সেটাই আবার ছোড়ে
কে কী করছে (হার উদাহরণমাত্র)
main thread
পেছনের কাজ
shared state
f.valid()
  1. কাজ দেওয়া, রসিদ নেওয়া। std::async ডাকার সাথে সাথে ফেরে — হিসাব শেষ হওয়ার জন্য অপেক্ষা করে না। std::launch::async বলায় কাজটি একটি আলাদা thread-এ শুরু হলো। f হলো রসিদ; পেছনের shared state এখনো খালি।
  2. দুজন একসাথে কাজ করছে। main thread স্ক্রিন আঁকছে, আর পেছনের thread হার হিসাব করছে। এই মুহূর্তটুকুই async-এর লাভ — দুটি কাজের সময় যোগ না হয়ে একে অপরের ওপর চাপা পড়ছে।
  3. get() — ফল এখনো আসেনি, তাই অপেক্ষা। main-এর নিজের কাজ শেষ, এখন হারটি লাগবে। ফল তৈরি নেই, তাই get() main thread-কে আটকে রাখে — যেন দর্জির কাউন্টারে দাঁড়িয়ে আছেন। (ফল আগেই তৈরি থাকলে get() সাথে সাথে ফিরত।)
  4. কাজ শেষ — ফল shared state-এ। পেছনের ফাংশন 117.5 return করল। লাইব্রেরি মানটি shared state-এ রাখল আর সেটিকে “ready” চিহ্ন দিল। অপেক্ষায় থাকা main thread-কে জাগানো হলো।
  5. ফল হাতে, রসিদ শেষ। get() মানটি বের করে দিল, আর main প্রিন্ট করল। এখন f.valid() হলো false — একই রসিদে দ্বিতীয়বার পাঞ্জাবি চাওয়া যায় না। আবার get() ডাকা undefined behavior.
  6. Exception-এর পথ। ধরুন fetchRate() একটি std::runtime_error ছুড়ল। সেটি পেছনের thread-এ হারিয়ে যায় না, প্রোগ্রামও বন্ধ হয় না — লাইব্রেরি exception-টি ধরে shared state-এ জমা রাখে। main যখন get() ডাকে, ঠিক সেই exception আবার ছোড়া হয়, যেন সেটা get()-এর লাইনেই ঘটেছে। তাই get()-এর চারপাশে স্বাভাবিক try/catch দিলেই চলে।

শেষ ধাপটি বিশেষ গুরুত্বপূর্ণ। পর্ব ৪৫-এ দেখেছেন, একটি সাধারণ std::thread-এর ফাংশন থেকে exception বেরিয়ে গেলে সাথে সাথে std::terminate — পুরো প্রোগ্রাম বন্ধ। future সেই ভুলকে একটি সাধারণ C++ exception-এ রূপ দেয়, যা পর্ব ৩৭-এর মতোই catch করা যায়। ভেতরে লাইব্রেরি এটা করে std::exception_ptr দিয়ে — একটি exception-কে এক thread থেকে ধরে আরেক thread-এ নিয়ে যাওয়ার পাত্র।

future-এর wait_for দিয়ে খোঁজ নেওয়ার উদাহরণ:

using namespace std::chrono_literals;
while (rate.wait_for(100ms) != std::future_status::ready)
    showSpinner();                 // প্রতি ১০০ ms-এ একবার ঘুরন্ত চিহ্ন
double r = rate.get();

সাবধান: rate যদি deferred হয়, wait_for সবসময় deferred ফেরত দেবে, কখনো ready নয় — লুপটি চিরকাল ঘুরবে। policy না দিলে এমন হতেই পারে। এটি std::launch::async স্পষ্ট লেখার আরেকটি কারণ।

৩ · ফেলে দেওয়া future-এর ফাঁদ — destructor-এ অপেক্ষা

এখন সেই আচরণটি, যা অভিজ্ঞ প্রোগ্রামারদেরও অবাক করে। একটি অনলাইন দোকানে অর্ডার হলে দুটি কাজ “পেছনে” করতে চান: log ফাইলে লেখা আর গ্রাহককে email পাঠানো। ফল দরকার নেই, তাই কেউ লেখেন:

std::async(std::launch::async, saveLog);     // future রাখা হলো না
std::async(std::launch::async, sendEmail);   // ভাবছেন দুটো একসাথে চলবে...

দেখতে নিরীহ। কিন্তু নিয়মটি হলো: std::async থেকে পাওয়া future-এর destructor, কাজটি শেষ না হওয়া পর্যন্ত অপেক্ষা করে (যদি সেটিই ওই shared state-এর শেষ রসিদ হয়)। আপনি future কোনো নামে রাখেননি, তাই সেটি একটি temporary — লাইনের শেষ ;-এ পৌঁছালেই ধ্বংস হয়। ধ্বংসের সময় সে অপেক্ষা করে। ফলে দ্বিতীয় লাইন শুরুই হয় প্রথম কাজ শেষ হওয়ার পর। ধাপে ধাপে দেখুন:

std::async(std::launch::async, saveLog);কাজ শুরু, future একটি temporary
সময় →  0s    1s    2s    3s    4s
main : [async]?
w1   : [saveLog █████]
future-এর নাম নেই — ; পর্যন্তই আয়ু
; // temporary future-এর destructordestructor কাজ শেষের অপেক্ষায়
সময় →  0s    1s    2s    3s    4s
main : [~~ অপেক্ষা ~~]
w1   : [saveLog █████]
main পরের লাইনে যেতেই পারছে না
std::async(std::launch::async, sendEmail);দ্বিতীয় কাজ শুরু হলো দেরিতে
সময় →  0s    1s    2s    3s    4s
main : [~~~~~~~~~~~~][~~~~~~~~~~~]
w1   : [saveLog ████]
w2   :              [sendEmail ██]
মোট ≈ 4s — thread দুটি, কাজ ক্রমিক!
auto f1 = std::async(...); auto f2 = std::async(...);সমাধান: রসিদ নামে রাখুন
সময় →  0s    1s    2s    3s    4s
main : [f1][f2] অন্য কাজ, তারপর get()
w1   : [saveLog ████]
w2   : [sendEmail ██]
মোট ≈ 2s — দুটো সত্যিই একসাথে
{ auto f = std::async(...); ... } // f-এর scopeঅপেক্ষার জায়গা = future-এর আয়ু
async-এর future    → অপেক্ষা করে
promise-এর future  → করে না
packaged_task-এর   → করে না
“fire and forget” async দিয়ে হয় না
  1. প্রথম কাজ শুরু। std::async saveLog-কে আলাদা thread-এ চালু করল আর একটি future ফেরত দিল। কিন্তু future-টি কোনো ভ্যারিয়েবলে রাখা হয়নি — এটি একটি temporary, যার আয়ু এই statement-এর শেষ পর্যন্ত।
  2. Temporary-র মৃত্যু, আর অপেক্ষা। ;-এ পৌঁছাতেই temporary future ধ্বংস হলো। এটিই shared state-এর শেষ রসিদ, আর কাজটি তখনো চলছে — তাই destructor saveLog শেষ হওয়া পর্যন্ত main-কে আটকে রাখল। (কেন এমন নকশা? পেছনের কাজটি হয়তো এমন local ডেটা ব্যবহার করছে, যা পরের লাইনেই মুছে যাবে। অপেক্ষা না করলে সেই ডেটা “ঝুলন্ত” হয়ে যেত।)
  3. দ্বিতীয় কাজ — দেরিতে শুরু। প্রথমটি শেষ হলে তবেই দ্বিতীয় লাইন চলল, আর সেখানেও একই ঘটনা। দুটি thread তৈরি হলো, কিন্তু কাজ চলল একটির পর একটি — মোট সময় দুটির যোগফল। কোনো error, কোনো warning নেই; প্রোগ্রাম শুধু ধীর। এমন বাগ profiler ছাড়া ধরা কঠিন।
  4. সমাধান — রসিদ নামে রাখুন। দুটি future-কে নাম দিন (f1, f2). এখন দুটি কাজ প্রায় একই মুহূর্তে শুরু হয়, আর main অপেক্ষা করে শুধু তখন, যখন get() ডাকে বা future-গুলোর scope শেষ হয়। মোট সময় প্রায় দীর্ঘতর কাজটির সমান।
  5. নিয়মটা মনে রাখুন। অপেক্ষার জায়গা ঠিক করে future-এর আয়ু। এই “destructor-এ অপেক্ষা” শুধু std::async-এর future-এর বৈশিষ্ট্য; std::promise বা std::packaged_task থেকে পাওয়া future ধ্বংসের সময় অপেক্ষা করে না। তাই async দিয়ে “চালিয়ে দাও আর ভুলে যাও” হয় না — সত্যিকারের পেছনের কাজের জন্য একটি দীর্ঘজীবী worker বা thread pool লাগে।

C++20 থেকে লাইব্রেরি std::async-কে [[nodiscard]] চিহ্ন দিয়েছে, তাই আধুনিক GCC আর Clang উপরের প্রথম কোডে একটি warning দেয়: return value ফেলে দেওয়া হচ্ছে। -Wall -Wextra চালু রাখার এটি আরেকটি কারণ। warning-টিকে (void) cast দিয়ে চুপ করাবেন না — সেটা সমস্যা লুকায়, সমাধান করে না।

C জানলে

C11-এও একই memory model আর atomic আছে — <stdatomic.h>-এর _Atomic int, atomic_fetch_add, আর একই memory_order_* নাম; C++11 আর C11 নকশাটি একসাথে পেয়েছে। কিন্তু C-তে future, promise বা async নেই: pthread-এ ফল ফেরত আনতে pthread_join-এর void** বা নিজের বানানো struct লাগে, আর exception-ই নেই। C কোর্সের মাল্টিথ্রেডিং পর্বে pthread আর C-এর atomic দেখে নিলে বুঝবেন, future কতটা ঝামেলা কমায়।

৪ · ভাগ করে যোগ — চার টুকরোয় বিক্রির হিসাব

একটি সুপারশপ চেইনের দশ লাখ বিক্রির রেকর্ড আছে, মোট টাকা বের করতে হবে। একজন হিসাবরক্ষক পুরো খাতা যোগ করলে যে সময় লাগে, চারজন চার ভাগ করে যোগ করে শেষে চারটি সংখ্যা মেলালে তার প্রায় চার ভাগের এক ভাগ সময় লাগে (যদি CPU-তে অন্তত চারটি core থাকে)। async দিয়ে এই নকশা খুব পরিষ্কারভাবে লেখা যায় — প্রতিটি টুকরোর জন্য একটি রসিদ, শেষে রসিদগুলো ভাঙানো। একই প্রোগ্রামে exception-এর পথও আছে:

#include <future>
#include <numeric>
#include <print>
#include <stdexcept>
#include <vector>
long long sumPart(const std::vector<int>& v, std::size_t from, std::size_t to) {
    return std::accumulate(v.begin() + from, v.begin() + to, 0LL);
}
int main() {
    std::vector<int> sales(1'000'000);
    for (std::size_t i = 0; i < sales.size(); ++i) sales[i] = i % 100 + 1;
    const std::size_t step = sales.size() / 4;
    std::vector<std::future<long long>> parts;
    for (std::size_t i = 0; i < 4; ++i)
        parts.push_back(std::async(std::launch::async, sumPart, std::cref(sales), i * step, (i + 1) * step));
    long long total = 0;
    for (auto& f : parts) total += f.get();      // wait for each part
    std::println("total sales = {}", total);
    auto rate = std::async(std::launch::async, []() -> double { throw std::runtime_error("rate server down"); });
    try { std::println("rate = {}", rate.get()); }
    catch (const std::exception& e) { std::println("caught: {}", e.what()); }
}

আউটপুট দুই লাইন: total sales = 50500000 আর caught: rate server down. প্রতিটি বিক্রি ১ থেকে ১০০ টাকার মধ্যে ঘুরে ঘুরে আসে, তাই যোগফল সহজে মিলিয়ে নেওয়া যায় — প্রতি ১০০টিতে ৫০৫০, দশ হাজার বার। খেয়াল করুন তিনটি জিনিস। এক, future-গুলো একটি vector-এ রাখা হয়েছে — রসিদ ফেলে দেওয়া হয়নি, তাই চারটি কাজ সত্যিই একসাথে চলে। দুই, std::cref(sales): std::async তার argument-গুলো ডিফল্টভাবে কপি করে নেয় (যাতে পেছনের কাজ ঝুলন্ত reference না পায়)। দশ লাখ সংখ্যার কপি এড়াতে আমরা স্পষ্টভাবে একটি const reference পাঠিয়েছি — আর দায়িত্ব নিয়েছি যে sales সব get() শেষ হওয়া পর্যন্ত বেঁচে থাকবে। তিন, lambda-র ভেতরের exception পেছনের thread-এ ঘটেছে, তবু ধরা পড়েছে main-এর catch-এ। আমরা কোথাও সময় বা thread-এর পরিচয় প্রিন্ট করিনি, তাই আউটপুট প্রতিবার একই।

এই নকশার নাম “ভাগ করো, আলাদা হিসাব করো, শেষে মেলাও” — fork-join. দুটি বাস্তব সতর্কতা মনে রাখুন। প্রথমত, প্রতিটি টুকরো যথেষ্ট বড় হওয়া চাই। একটি thread চালু করতে কয়েক মাইক্রোসেকেন্ড লাগে; একশোটি সংখ্যা যোগের জন্য নতুন thread খোলা মানে এক কাপ চায়ের জন্য পুরো রান্নাঘর জ্বালানো। দ্বিতীয়ত, core-এর চেয়ে বেশি টুকরো করলে লাভ বাড়ে না। std::thread::hardware_concurrency() একটি ইঙ্গিত দেয়, কতগুলো thread আসলে একসাথে চলতে পারে (এটি ০-ও ফেরত দিতে পারে, যদি জানা না যায়)। আর এই যোগের কাজটি আসলে লাইব্রেরি নিজেই করে দিতে পারে — অনুচ্ছেদ ৮-এ std::reduce দেখবেন।

৫ · promise, packaged_task আর shared_future — হাতে হাতে হস্তান্তর

std::async সুবিধাজনক, কারণ রসিদ আর কাজ একসাথে বানায়। কিন্তু কখনো ফল তৈরি হয় এমন জায়গায়, যা “একটি ফাংশনের return value” নয় — যেমন একটি দীর্ঘজীবী thread কাজের মাঝপথে একটি মান জানায়, অথবা ফলটি আসে একটি callback থেকে। তখন রসিদের দুই প্রান্ত আলাদা করে হাতে নিতে হয়।

std::promise — লেখার প্রান্ত

std::promise<T> হলো সেই প্রতিশ্রুতি, যে “একদিন একটি T দেব”। তার কাছ থেকে get_future() দিয়ে একটি future নেওয়া যায় — পড়ার প্রান্ত। যে thread ফল বানায়, সে set_value(v) ডাকে (বা ভুল হলে set_exception). দুই প্রান্তের মাঝে সেই একই shared state:

std::promise<std::string> p;
std::future<std::string>  f = p.get_future();     // পড়ার প্রান্ত

std::jthread loader([&p] {
    std::string cfg = readConfigFile();             // ধীর কাজ
    p.set_value(std::move(cfg));                    // ফল পাঠানো — f এখন ready
    keepWatchingForChanges();                       // thread চলতেই থাকে
});
std::println("config: {}", f.get());                // ready হলেই পাবেন

এখানে loader thread ফল পাঠানোর পরেও কাজ চালিয়ে যায় — std::async দিয়ে এটা প্রকাশ করা যেত না, কারণ async-এর ফল আসে কেবল ফাংশন শেষ হলে। দুটি নিয়ম মনে রাখুন। set_value একবারই ডাকা যায় (দ্বিতীয়বার std::future_error). আর promise যদি কোনো মান না দিয়েই ধ্বংস হয়, তবে future-এর get() একটি std::future_error ছোড়ে, যার কারণ broken_promise — দর্জি দোকান বন্ধ করে চলে গেছেন, রসিদ এখন অচল। অপেক্ষাকারী তাই চিরকাল আটকে থাকে না।

std::packaged_task — কাজ আর রসিদ একসাথে মোড়ানো (এক ঝলক)

std::packaged_task একটি ফাংশনকে মুড়ে দেয়, যাতে সেটি ডাকা হলে return value নিজে থেকেই একটি promise-এ যায়। পার্থক্য হলো, কখন আর কোথায় ডাকা হবে, তা আপনি ঠিক করেন — যেমন একটি কাজের সারিতে (queue) রেখে দিয়ে, যা পরে কোনো worker thread তুলে নিয়ে চালাবে। thread pool-গুলো ঠিক এভাবেই ভেতরে future ফেরত দেয়।

std::packaged_task<int(int)> task([](int x) { return x * x; });
std::future<int> f = task.get_future();
jobQueue.push(std::move(task));     // পরে কোনো worker ডাকবে: task(12)
// ... f.get() == 144

std::shared_future — এক ফল, অনেক পাঠক

সাধারণ future-এর ফল একজনই তুলতে পারে, আর future নিজে কপি করা যায় না (শুধু move). কিন্তু ধরুন একটি অ্যাপের পাঁচটি অংশ — মেনু, ছবি, ভাষা, থিম, নোটিফিকেশন — সবাই একই কনফিগারেশনের জন্য অপেক্ষা করছে। এর জন্য std::shared_future: f.share() দিয়ে বানানো যায়, কপি করা যায়, আর প্রতিটি কপি যতবার খুশি get() ডাকতে পারে — ফল ফেরত আসে const reference হিসেবে। নিয়ম একটাই: প্রতিটি thread নিজের কপি রাখবে; একই shared_future object-কে একাধিক thread থেকে একসাথে ছোঁয়া নিরাপদ নয়, কিন্তু আলাদা কপি একই shared state পড়তে পারে।

std::shared_future<Config> cfg = p.get_future().share();
auto widget = [cfg](std::string name) {        // প্রত্যেকে নিজের কপি নেয়
    const Config& c = cfg.get();               // সবাই একই ফল পায়
    draw(name, c.theme);
};
প্রয়োজনযন্ত্র
একটি ফাংশনের ফল পেছনে হিসাব করে পরে নেবstd::async + std::future
ফল আসবে কাজের মাঝপথে বা callback থেকেstd::promise
কাজটি এখন মুড়ে রাখব, পরে কেউ চালাবে (queue, pool)std::packaged_task
একই ফল অনেক thread পড়বেstd::shared_future
বারবার সংকেত, বা অনেক ডেটার সারিfuture নয় — condition_variable বা সারি (পর্ব ৪৫)

শেষ সারিটি মনে রাখুন: future একবারের হস্তান্তর — একটি রসিদ, একটি পাঞ্জাবি। বারবার ঘটা ঘটনার জন্য এটি নয়।

৬ · std::atomic — ভাঙা যায় না এমন ছোট কাজ

পর্ব ৪৫-এ দেখেছেন, দুটি thread একসাথে hits++ করলে গোনা হারায়। কারণ ++ আসলে তিনটি ধাপ — মেমরি থেকে পড়া, এক যোগ করা, ফিরিয়ে লেখা — আর দুই thread-এর ধাপগুলো মিশে যেতে পারে। এটি একটি data race, আর C++-এ সেটি undefined behavior. mutex দিয়ে সমাধান হয়, কিন্তু শুধু একটি সংখ্যা বাড়াতে পুরো তালা অনেক সময় বাড়াবাড়ি।

std::atomic<T> (<atomic> header) একটি ভ্যারিয়েবলকে এমন বানায়, যার ওপর প্রতিটি কাজ একটি atomic operation — অন্য কোনো thread মাঝপথ দেখতে পায় না; হয় পুরো কাজটা হয়েছে, নয়তো একটুও হয়নি। বিকাশ বা নগদ অ্যাকাউন্টের ব্যালান্সের কথা ভাবুন: দুজন একই মুহূর্তে টাকা পাঠালে সিস্টেম কখনো “অর্ধেক যোগ” অবস্থা দেখায় না। সাধারণ x86 বা ARM CPU-তে এগুলো সাধারণত একটি বিশেষ CPU নির্দেশ দিয়েই হয়ে যায়, তালা ছাড়া।

কাজমানে
a.load() / a.store(v)পুরো মানটি এক ধাক্কায় পড়া / লেখা — কখনো আধা-লেখা মান দেখা যায় না
a.fetch_add(n), fetch_subপড়া + যোগ + লেখা একসাথে; আগের মান ফেরত দেয়। a++, a += n একই কাজ করে
a.exchange(v)নতুন মান বসানো, আর পুরোনোটা ফেরত দেওয়া
a.compare_exchange_weak(exp, v)“মান যদি এখনো exp থাকে, তবেই v বসাও” — না মিললে বসায় না, বরং বর্তমান মান exp-এ লিখে দেয়

শেষ সারিটির নাম CAS (compare-and-swap), আর এটি সব lock-free কৌশলের মেরুদণ্ড। কেন দরকার? “সবচেয়ে ধীর response time” রাখতে চাই — প্রতিটি request নিজের সময় দেখে, বড় হলে সর্বোচ্চ মানটি বদলায়। C++23 পর্যন্ত fetch_add-এর মতো কোনো তৈরি fetch_max নেই (নতুন standard-এ যোগ হচ্ছে), তাই “পড়ো, তুলনা করো, লেখো” — এই তিন ধাপকে নিরাপদ করতে হয় CAS লুপ দিয়ে। নিচের প্রোগ্রামে চারটি thread একটি website-এর hit counter বাড়ায় আর একই সাথে সবচেয়ে ধীর response time আপডেট করে:

#include <atomic>
#include <print>
#include <thread>
#include <vector>
std::atomic<long> hits{0};          // website hit counter
std::atomic<int> slowest{0};        // slowest response time in ms
void updateMax(int ms) {
    int cur = slowest.load();
    while (cur < ms && !slowest.compare_exchange_weak(cur, ms)) { }  // fail: cur = newer value, retry
}
int main() {
    {   std::vector<std::jthread> workers;
        for (int t = 0; t < 4; ++t)
            workers.emplace_back([t] {
                for (int i = 0; i < 100'000; ++i) {
                    hits.fetch_add(1, std::memory_order_relaxed);
                    updateMax((i * 7 + t * 13) % 997);
                }
            });
    }                                   // every jthread joins here
    std::println("hits = {}, slowest = {} ms, lock-free: {}", hits.load(), slowest.load(), hits.is_lock_free());
}

আউটপুট: hits = 400000, slowest = 996 ms, lock-free: true. চারটি thread × এক লাখ hit — একটিও হারায়নি, কোনো mutex ছাড়াই। response time-গুলো আমরা একটি নির্দিষ্ট সূত্রে বানিয়েছি (০ থেকে ৯৯৬-এর মধ্যে), তাই সর্বোচ্চ মান সবসময় ৯৯৬ — thread-গুলো যে ক্রমেই চলুক। counter-এ memory_order_relaxed দেওয়া হয়েছে, কারণ এখানে শুধু গোনা দরকার, অন্য কোনো ডেটার সাথে ক্রমের সম্পর্ক নেই (অনুচ্ছেদ ৭ দেখুন)। শেষে পড়ার আগে ভেতরের {} শেষ হয়েছে, তাই সব jthread join হয়ে গেছে। is_lock_free() জানায় এই টাইপের atomic এই মেশিনে CPU নির্দেশ দিয়েই চলে, ভেতরে লুকানো তালা ছাড়া — সাধারণ ৬৪-বিট মেশিনে long-এর জন্য উত্তর true.

updateMax-এর লুপটি প্রথমবার দেখলে অদ্ভুত লাগে। দুটি thread একই মুহূর্তে আলাদা মান নিয়ে এলে কী ঘটে, ধাপে ধাপে দেখুন:

1int cur = slowest.load();2while (cur < ms &&3       !slowest.compare_exchange_weak(cur, ms)) { }4// ভুল পথ: if (slowest < ms) slowest = ms;
thread A (ms = 700), thread B (ms = 900)
slowest
A-এর cur
B-এর cur
ফল
  1. দুজনেই পুরোনো মান পড়ল। slowest এখন 500. thread A-এর কাছে 700, thread B-এর কাছে 900 — দুজনেরই মান বড়, দুজনেই বদলাতে চায়। দুজনের cur-এ 500.
  2. B আগে CAS করল — সফল। B বলল: “মান যদি এখনো 500 থাকে, 900 বসাও।” মান 500-ই ছিল, তাই এক অবিভাজ্য ধাপে 900 বসল। CAS true ফেরত দিল, B-এর লুপ শেষ।
  3. A-এর CAS ব্যর্থ — কিন্তু নতুন খবর পেল। A বলল: “মান যদি এখনো 500 থাকে, 700 বসাও।” কিন্তু মান এখন 900 — শর্ত মিলল না, কিছুই লেখা হলো না। ব্যর্থ CAS বর্তমান মান (900) A-এর cur-এ লিখে দেয়। আলাদা করে আবার load লাগে না।
  4. আবার যাচাই — আর দরকার নেই। লুপের শর্ত আবার দেখা হলো: 900 < 700? না। তাই A থেমে গেল, কিছু না লিখেই। ফল সঠিক: 900. কোনো তালা নেই, কেউ অপেক্ষায় বসে থাকেনি — যে হেরেছে, সে শুধু নতুন অবস্থা দেখে আবার সিদ্ধান্ত নিয়েছে। (_weak সংস্করণ মাঝে মাঝে মান মিললেও “মিছিমিছি” ব্যর্থ হতে পারে — spurious failure — তাই একে সবসময় লুপে রাখা হয়।)
  5. CAS ছাড়া হলে কী হতো। লাইন ৪-এর মতো সাধারণ “পড়ো, তুলনা করো, লেখো” লিখলে (atomic হলেও, ধাপগুলো আলাদা): দুজনেই 500 পড়ে, B 900 লেখে, তারপর A তার পুরোনো তুলনার ভরসায় 700 লিখে দেয়। B-এর আপডেট হারিয়ে গেল — একে বলে lost update. প্রতিটি আলাদা ধাপ atomic হলেও তিন ধাপ মিলে একটি কাজ atomic হয় না। CAS তুলনা আর লেখাকে একটি ধাপে জুড়ে দেয়।

atomic না mutex?

প্রশ্নstd::atomicstd::mutex
কী রক্ষা করেএকটি মাত্র ভ্যারিয়েবলযত খুশি ডেটা, একসাথে
উদাহরণcounter, পতাকা, সর্বোচ্চ মান, pointerব্যালান্স কমানো আর লেনদেনের তালিকায় যোগ — দুটো একসাথে
অপেক্ষাসাধারণত ঘুমায় না; CAS লুপে আবার চেষ্টাতালা না পেলে thread ঘুমাতে পারে
খরচপ্রতিযোগিতা কম হলে খুব কমপ্রতিযোগিতা না থাকলে এটিও বেশ সস্তা
ভুলের ঝুঁকিএকাধিক atomic মিলিয়ে নিয়ম রাখতে গেলে খুব বেশিকম — ভেতরের কোড সাধারণ ক্রমিক কোডের মতো

সবচেয়ে বড় শিক্ষা দ্বিতীয় সারিতে। দুটি আলাদা atomic — balance আর txCount — প্রত্যেকটি নিজে নিরাপদ, কিন্তু “দুটো একসাথে বদলাও” নিরাপদ নয়: মাঝখানে অন্য thread একটি বদলানো আর একটি পুরোনো মান দেখতে পারে। একাধিক ভ্যারিয়েবলের মধ্যে কোনো নিয়ম (invariant) রাখতে হলে mutex নিন। আর atomic-ও বিনামূল্যে নয়: অনেক core একই atomic-এ বারবার লিখলে সেই cache line core থেকে core-এ ছোটাছুটি করে। দুটি আলাদা atomic একই cache line-এ পড়লেও একই সমস্যা হয় — একে বলে false sharing.

আরও কিছু atomic যন্ত্র

  • lock-free আর is_lock_free(): std::atomic<T> যেকোনো trivially copyable টাইপে বানানো যায়, কিন্তু বড় টাইপের জন্য লাইব্রেরি ভেতরে গোপনে একটি তালা ব্যবহার করতে পারে। is_lock_free() চলার সময় জানায়, আর is_always_lock_free (C++17) compile-এর সময়েই।
  • std::atomic_flag: সবচেয়ে সরল atomic — শুধু একটি বুলিয়ান পতাকা। standard একমাত্র এটিকেই সব প্ল্যাটফর্মে lock-free হওয়ার নিশ্চয়তা দেয়। test_and_set() আর clear() দিয়ে একটি ক্ষুদ্র spinlock বানানো যায়; C++20 থেকে ডিফল্টভাবে তৈরি flag “খালি” অবস্থায় থাকে আর test() দিয়ে শুধু পড়াও যায়।
  • C++20-এর wait/notify: a.wait(old) thread-কে ঘুম পাড়িয়ে রাখে, যতক্ষণ মান old থাকে; অন্য thread মান বদলে a.notify_one() বা notify_all() ডাকলে জাগে। ফলে লুপে CPU না পুড়িয়ে একটি পতাকার জন্য অপেক্ষা করা যায় — অনেকটা হালকা condition_variable-এর মতো।
  • atomic<shared_ptr<T>> (C++20): পর্ব ১৭-এ দেখেছেন, shared_ptr-এর গোনা atomic হলেও একই shared_ptr ভ্যারিয়েবলকে দুই thread থেকে একসাথে বদলানো নিরাপদ নয়। এই টাইপ সেটি নিরাপদ করে — যেমন একটি কনফিগারেশন, যা এক thread পুরোটা নতুন করে বসায় আর অন্যরা পড়ে। এটি সাধারণত lock-free হয় না, তবু হাতে লেখা কৌশলের চেয়ে অনেক নিরাপদ।

৭ · Memory ordering — আগে ডেটা রাখুন, তারপর পতাকা তুলুন

এবার একটি গভীর কিন্তু জরুরি বিষয়। একটি অ্যাপে loader thread ডিস্ক থেকে একটি রিপোর্ট পড়ে, আর UI thread অপেক্ষা করে একটি “ডেটা তৈরি” পতাকার জন্য। স্বাভাবিক বুদ্ধিতে লেখা হয়: আগে ডেটা লিখি, তারপর পতাকা true করি। UI পতাকা true দেখলে ডেটা পড়বে। এতে ভুল কোথায়?

ভুল হলো, আপনার লেখা ক্রমই যে আসল ক্রম, এমন নিশ্চয়তা নেই। compiler দ্রুত করার জন্য স্বাধীন দুটি লেখার ক্রম বদলে দিতে পারে, আর CPU-ও লেখাগুলো নিজের store buffer-এ জমিয়ে রেখে ভিন্ন ক্রমে মেমরিতে পাঠাতে পারে — একে বলে reordering. এক thread-এর ভেতরে এটা টের পাওয়া যায় না, কারণ ফল একই থাকে। কিন্তু অন্য thread থেকে দেখলে পতাকা আগে, ডেটা পরে পৌঁছাতে পারে। এই নিয়মগুলোর পুরো বিবরণকে বলে C++-এর memory model, আর প্রতিটি atomic কাজে আপনি বলে দিতে পারেন কতটা ক্রম রক্ষা চাই — memory ordering.

seq_cst — ডিফল্ট

সব thread সব seq_cst atomic কাজ একটিই সাধারণ ক্রমে দেখে, যেন সবকিছু একটি লাইনে ঘটছে। বোঝা সবচেয়ে সহজ, তাই এটিই ডিফল্ট। কিছু CPU-তে সামান্য ধীর।

safe default

acquire / release

দুই thread-এর মাঝে হস্তান্তর: acquire-load যদি release-store-এর লেখা মানটি দেখে, তবে store-এর আগের সব লেখা load-এর পরে অন্য thread-এ দেখা যাবে। “আগে ডেটা, তারপর পতাকা”।

handshake

relaxed

শুধু এই ভ্যারিয়েবলটির কাজ atomic — অন্য কোনো ডেটার সাথে ক্রমের কোনো নিশ্চয়তা নেই। খাঁটি counter বা পরিসংখ্যানের জন্য।

counting only

ডিফল্ট sequential consistency (memory_order_seq_cst) দিয়ে উপরের পতাকার কোড এমনিতেই সঠিক — ডেটা আর পতাকা আপনার লেখা ক্রমেই দেখা যাবে, যতক্ষণ পতাকাটি std::atomic<bool>. হাতে ক্রম বলতে চাইলে সবচেয়ে প্রচলিত জোড়া হলো acquire/release. দুই thread-এর হস্তান্তরটি ধাপে ধাপে দেখুন:

Report data; std::atomic<bool> ready{false};দুই thread, একটি পতাকা
loader thread               UI thread
─────────────               ─────────
                            while (!ready.load(acquire))
                                ;   // অপেক্ষা
data   = (খালি)
ready  = false
data = loadReport();সাধারণ লেখা — এখনো কারও দেখার কথা নয়
loader thread               UI thread
─────────────               ─────────
data = loadReport(); ✓      (এখনো অপেক্ষা)
     │
     ▼  “এর আগের সব লেখা
        পতাকার সাথে যাবে”
ready.store(true, std::memory_order_release);পতাকা তোলা = প্রকাশ
loader thread               UI thread
─────────────               ─────────
data = loadReport();
ready.store(true, release) ──┐
   ▲ উপরের কোনো লেখা         │  “প্যাকেটে” ডেটাসহ
     এর নিচে নামতে পারে না   └──────────►
ready.load(std::memory_order_acquire) == trueacquire সেই মানটি দেখল
loader thread               UI thread
─────────────               ─────────
data = loadReport();
ready.store(true, release) ───────► ready.load(acquire) → true
                                    show(data);  → পূর্ণ রিপোর্ট ✓
                            ▲ নিচের কোনো পড়া এর উপরে
                              উঠতে পারে না
bool ready; অথবা store(true, relaxed)ভুল সংস্করণ — পুরোনো মান দেখা
loader thread               UI thread
─────────────               ─────────
ready = true;   ← আগে পৌঁছাল!   ready → true
data = loadReport();               show(data); → খালি/আধা ✗
compiler বা CPU ক্রম বদলেছে। সাধারণ bool হলে পতাকাতেই
data race; relaxed হলে পতাকা ঠিক, কিন্তু data-র লেখা-পড়ায়
ক্রম নেই — data-তে data race, তাই এটিও undefined behavior.
  1. শুরুর অবস্থা। loader একটি রিপোর্ট বানাবে; UI thread একটি std::atomic<bool> পতাকার দিকে তাকিয়ে আছে। data নিজে সাধারণ (non-atomic) ভ্যারিয়েবল — বড় রিপোর্টকে atomic বানানোর দরকার নেই, পতাকাই পাহারাদার।
  2. আগে ডেটা লেখা। loader রিপোর্টটি data-তে লিখল। এগুলো সাধারণ লেখা; এই মুহূর্তে অন্য thread এগুলো পড়লে সেটা data race হতো। তাই UI এখনো পড়ে না — সে শুধু পতাকা দেখছে।
  3. Release — পতাকা তোলা। store(true, release) একটি প্রতিশ্রুতি দেয়: এর আগের কোনো লেখা (data-সহ) এর পরে সরে যেতে পারবে না — compiler-ও না, CPU-ও না। যেন কুরিয়ারের প্যাকেট সিল করার আগে ভেতরে সব জিনিস ঢোকানো নিশ্চিত করা হলো।
  4. Acquire — পতাকা দেখা, তারপর ডেটা। UI-এর load(acquire) যখন সেই true মানটি দেখে, তখন একটি happens-before সম্পর্ক তৈরি হয়: release-এর আগের সব লেখা এখন UI-এর কাছে দৃশ্যমান। আর acquire-এর পরের কোনো পড়া এর আগে উঠে আসতে পারে না। তাই show(data) নিশ্চিতভাবে পূর্ণ রিপোর্ট দেখে।
  5. ভুল সংস্করণ। পতাকা সাধারণ bool হলে, দুই thread একই ভ্যারিয়েবলে লেখা-পড়া করছে — data race, undefined behavior. compiler এমনকি UI-এর লুপটিকে “পতাকা কখনো বদলায় না” ধরে চিরস্থায়ী লুপ বানিয়ে দিতে পারে। আর atomic হলেও relaxed দিলে পতাকাটা ঠিকঠাক আসে, কিন্তু data-র সাথে কোনো ক্রমের নিশ্চয়তা থাকে না — loader-এর লেখা আর UI-এর পড়া তখন data-তে data race, অর্থাৎ এটিও undefined behavior. বাস্তবে UI পতাকা true দেখেও পুরোনো (stale) বা আধা-লেখা ডেটা পেতে পারে। দুর্বল ক্রমের CPU-তে (যেমন অনেক ARM ফোন) এটা বাস্তবে ঘটে; x86 ল্যাপটপে হয়তো কখনো দেখবেন না — আর সেটাই বিপদ।

তাহলে memory_order_relaxed কোথায় চলে? যেখানে ভ্যারিয়েবলটি নিজেই সব, অন্য কোনো ডেটা পাহারা দেয় না — যেমন hit counter, যা শেষে সব thread join হওয়ার পর পড়া হয় (join নিজেই প্রয়োজনীয় ক্রম দেয়)। কিন্তু relaxed counter দেখে কোনো সিদ্ধান্ত নিয়ে অন্য ডেটা পড়লে, সেটা আর “শুধু গোনা” নয়।

সৎ সতর্কতা: memory ordering-এর বাকি অংশ — নিজের lock-free queue, stack বা hash table লেখা — বিশেষজ্ঞদের এলাকা। এসব কোডে বাগ থাকে, যা লাখবারে একবার, শুধু নির্দিষ্ট CPU-তে, চাপের মধ্যে দেখা দেয়, আর টেস্টে প্রায় ধরা পড়ে না। দক্ষ দলও এমন কোড লেখার পর মাসের পর মাস পর্যালোচনা করে, বিশেষ টুল দিয়ে যাচাই করে। আপনার জন্য বাস্তব নিয়ম: ডিফল্ট seq_cst রাখুন; acquire/release শুধু এই “পতাকা” নকশায়; relaxed শুধু খাঁটি counter-এ। এর বাইরে mutex, বা পরীক্ষিত লাইব্রেরি। আর সবসময় ThreadSanitizer (-fsanitize=thread) দিয়ে চালিয়ে দেখুন।

C++98 আর C++03 standard-এ “thread” শব্দটিই ছিল না। মানুষ তবু pthread বা Windows API দিয়ে বহু-thread প্রোগ্রাম লিখত, কিন্তু ভাষা কিছুই নিশ্চয়তা দিত না — compiler-এর কোন optimization thread-এর জন্য বৈধ, তা ঠিক করা ছিল অস্পষ্ট। ২০০৫ সালে Hans Boehm-এর প্রবন্ধ “Threads Cannot Be Implemented as a Library” দেখায়, শুধু লাইব্রেরি দিয়ে এ সমস্যা মেটানো যায় না; ভাষাকেই নিয়ম বলতে হবে। এরপর কয়েক বছরের কাজে C++11-এ (২০১১) একসাথে এল memory model, std::thread, std::atomic, mutex আর future/promise/async. একই memory model C11-এও গৃহীত হয়।

পরীক্ষা ও ইন্টারভিউতে
  • std::async আর std::thread-এর পার্থক্য? — async একটি future ফেরত দেয়, যা দিয়ে ফল আর exception দুটোই পাওয়া যায়; thread কোনো মান ফেরত দেয় না, আর তার ভেতরের অধরা exception প্রোগ্রাম বন্ধ করে।
  • future::get() কতবার ডাকা যায়? — একবার; এরপর valid() false. অনেক পাঠক লাগলে std::shared_future.
  • policy ছাড়া std::async কি নতুন thread-এ চলে? — নিশ্চিত নয়; ডিফল্ট async | deferred, লাইব্রেরি বেছে নেয়। নিশ্চিত করতে std::launch::async.
  • volatile কি thread-safe? — না। C++-এ volatile atomicity বা thread-এর মাঝে ক্রম কোনোটাই দেয় না; তার জন্য std::atomic.
  • CAS কী? — compare-and-swap: “মান এখনো X থাকলে Y বসাও” এক অবিভাজ্য ধাপে; ব্যর্থ হলে বর্তমান মান জানায়, তাই লুপে আবার চেষ্টা করা হয়।

৮ · বড় ছবি — parallel algorithm আর C++26-এর std::execution

অনুচ্ছেদ ৪-এ আমরা নিজে হাতে চার টুকরো করেছি। কিন্তু পর্ব ৩৩-এ দেখেছেন, C++17 থেকে অনেক STL algorithm-কে একটি execution policy দেওয়া যায়, আর লাইব্রেরি নিজেই কাজটা ভাগ করে নেয়:

#include <execution>
#include <numeric>

long long total = std::reduce(std::execution::par,
                              sales.begin(), sales.end(), 0LL);

একটি লাইনে একই কাজ — কয়টি টুকরো, কয়টি thread, সব লাইব্রেরির সিদ্ধান্ত। এখানে std::accumulate নয়, std::reduce, কারণ সমান্তরালে যোগ করতে হলে যোগের ক্রম বদলানোর অনুমতি লাগে। বাস্তব সতর্কতা: GCC-র লাইব্রেরিতে par সত্যিই সমান্তরালে চলতে Intel TBB লাইব্রেরি লাগে (link-এ -ltbb); না থাকলে কাজ ক্রমিকভাবেও চলতে পারে। আর policy-যুক্ত algorithm-এর ভেতর থেকে exception বেরোলে std::terminate হয় — future-এর মতো ফিরে আসে না।

আরেকটু দূরে তাকান। future-এর একটি বড় দুর্বলতা হলো, “এটা শেষ হলে ওটা করো” — এভাবে কাজ জোড়া লাগানো যায় না; get()-এ আটকে থাকতেই হয়। কাজের শৃঙ্খল বানানোর জন্য দীর্ঘ আলোচনার পর std::execution — যাকে senders/receivers বলা হয় (প্রস্তাব P2300) — C++26-এ গৃহীত হয়েছে। এতে একটি কাজ (sender) বর্ণনা করে কী করতে হবে, একটি scheduler ঠিক করে কোথায় চলবে (thread pool, GPU, event loop), আর কাজগুলো পাইপের মতো জোড়া লাগে:

// C++26 — ধারণা দেখানোর জন্য; compiler সমর্থন এখনো আসছে
namespace ex = std::execution;
auto work = ex::schedule(pool.get_scheduler())
          | ex::then([] { return fetchRate(); })
          | ex::then([](double r) { return r * 100; });
auto [result] = std::this_thread::sync_wait(std::move(work)).value();

এখানে কিছুই আগে থেকে চলে না — পুরো শৃঙ্খলটি একটি বর্ণনা, যা sync_wait চালু করে। ভুল আর বাতিল (cancellation) আলাদা পথে বয়ে যায়। পর্ব ৪২-এর coroutine-এর সাথে মিলে এটিই আধুনিক C++-এর async-এর ভবিষ্যৎ। আজ আপনার বাস্তব পছন্দের ক্রম: আগে parallel algorithm, তারপর std::async/future, তারপর thread ও mutex, আর একেবারে শেষে নিজের atomic কৌশল।

মূল নিয়ম

যত উঁচু স্তরের যন্ত্র দিয়ে কাজ হয়, তত ভালো। নিচের স্তরে নামবেন কেবল মেপে দেখার পর, যখন উপরের স্তর সত্যিই ধীর — আর নামলে সাথে নেবেন ThreadSanitizer আর পরীক্ষার কোড।

৯ · AI-এর যুগে — concurrent কোডের ভুল ধরা

concurrency-তে AI-এর লেখা কোড সবচেয়ে বিপজ্জনক, কারণ ভুল কোডও প্রায়ই টেস্টে পাস করে। আপনার ল্যাপটপে একশোবার চালালে ঠিক আসে; সার্ভারে লাখবারে একবার ভুল আসে। অনলাইনে পুরোনো রীতির (C++11-এর আগের, বা Java থেকে আনা ধারণার) কোড প্রচুর, আর AI সেগুলোই নকল করে। সাধারণ ভুলগুলো:

AI যা করেসমস্যাযা হওয়া উচিত
std::async(...) ডেকে future ফেলে দেয়destructor-এ অপেক্ষা — কাজ ক্রমিক হয়ে যায়future নামে রাখুন, পরে get()
policy ছাড়া std::async(f)deferred হতে পারে — সমান্তরাল নয়, wait_for লুপ চিরস্থায়ীstd::launch::async স্পষ্ট লিখুন
volatile bool done; বা volatile int count;C++-এ volatile thread-safe নয় — data race, UBstd::atomic<bool>, std::atomic<int>
“দ্রুততর” বলে সবখানে memory_order_relaxedপতাকা দেখেও পুরোনো ডেটা — শুধু ARM-এ ধরা পড়েডিফল্ট seq_cst; পতাকায় release/acquire
নিজের lock-free queue বা stack লেখেABA, memory reclamation, ordering — সূক্ষ্ম বাগে ভরাmutex-সহ সারি, বা পরীক্ষিত লাইব্রেরি
দুটি atomic আলাদাভাবে বদলায়, ধরে নেয় একসাথে বদলেছেমাঝখানে অন্য thread অসংগত অবস্থা দেখেএকাধিক ভ্যারিয়েবলের নিয়মে mutex
get()-এর চারপাশে try/catch নেইপেছনের exception main-এ অধরা থেকে প্রোগ্রাম বন্ধget()-কে exception-এর জায়গা ধরে সামলান

AI-কে concurrent কোড লিখতে বলার সময় নিয়মগুলো আগেই দিয়ে দিন, আর প্রতিটি ভাগ করা ভ্যারিয়েবলের পাহারাদার কে, তা ব্যাখ্যা করতে বলুন:

আমি C++23 শিখছি (g++, -Wall -Wextra). একটি ছোট প্রোগ্রাম লেখো: একটি ভ্রমণ অ্যাপ ডলারের হার আর আবহাওয়া একসাথে পেছনে হিসাব করবে (নেটওয়ার্ক নয়, শুধু হিসাব দিয়ে নকল করো), আর একটি hit counter থাকবে, যা চারটি thread বাড়াবে। নিয়ম: ১. পেছনের কাজে std::async, সবসময় std::launch::async দিয়ে; প্রতিটি future নামে রাখবে, কোনোটা ফেলে দেবে না। ২. পেছনের একটি কাজ ইচ্ছা করে exception ছুড়বে; get()-এর চারপাশে try/catch দিয়ে সেটা ধরবে। ৩. counter হবে std::atomic; কোথাও volatile লিখবে না। ৪. memory_order দিলে প্রতিটির পাশে এক লাইনে কারণ লেখো; না জানলে ডিফল্ট রাখো। ৫. নিজের lock-free কাঠামো বানাবে না। ৬. আউটপুটে সময় বা thread id প্রিন্ট করবে না, শুধু ফল। কোডের পরে একটি তালিকা দাও: প্রতিটি ভাগ করা ভ্যারিয়েবল, কোন thread পড়ে আর লেখে, আর কী (atomic, mutex, future) তাকে রক্ষা করছে।

AI-এর concurrent কোড যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১std::async-এর নকশা নিয়ে Scott Meyers, Bartosz Milewski-সহ অনেকে সমালোচনা করেছেন। আপত্তিগুলো কী, আর standard সেগুলো বদলায়নি কেন?

std::async এসেছিল C++11-এ, একটি সহজ প্রতিশ্রুতি নিয়ে: “thread নিয়ে ভাববেন না, কাজ দিন।” কিন্তু খুঁটিনাটি নিয়ে প্রায় শুরু থেকেই আপত্তি ওঠে। Bartosz Milewski ২০১১ সালে তাঁর ব্লগে লেখেন, async “এখনো পুরোপুরি তৈরি নয়”। তাঁর মূল আপত্তি ছিল ডিফল্ট policy নিয়ে: কাজটি আলাদা thread-এ চলবে, নাকি deferred হয়ে ডাকার thread-এই চলবে, তা জানা না থাকলে thread-local ভ্যারিয়েবল কোনটি দেখা যাবে, কোনো তালা ধরা থাকলে deadlock হবে কি না — এসব নিয়ে যুক্তি করা যায় না।

Scott Meyers ২০১৪ সালের Effective Modern C++ বইয়ে একে কয়েকটি আলাদা পরামর্শে ভাগ করেন। একদিকে বলেন, thread-ভিত্তিক নয়, task-ভিত্তিক প্রোগ্রামিং পছন্দ করুন — অর্থাৎ async-এর মূল ভাবনা ভালো। অন্যদিকে বলেন, সমান্তরালতা জরুরি হলে std::launch::async স্পষ্ট লিখুন, কারণ ডিফল্টে wait_for-ভিত্তিক লুপ চিরকাল ঘুরতে পারে। আর আলাদা করে সতর্ক করেন: বিভিন্ন thread-handle-এর destructor ভিন্ন আচরণ করে — async-এর future অপেক্ষা করে, অন্য future করে না, আর std::thread join না হলে terminate করে।

destructor-এর এই অপেক্ষা নিয়েই সবচেয়ে বড় বিতর্ক। ২০১৩ সালের দিকে Herb Sutter-সহ কয়েকজন প্রস্তাব দেন, async-এর future-ও অন্যদের মতো অপেক্ষা না করুক, কারণ এক টাইপের দুই রকম আচরণ বিভ্রান্তিকর। অন্যপক্ষ — Hans Boehm-সহ — যুক্তি দেন, অপেক্ষা না করলে পেছনের কাজটি এমন local ডেটা ছুঁতে পারে, যা ততক্ষণে মুছে গেছে; আর ইতিমধ্যে লেখা কোড নীরবে ভেঙে যাবে। শেষ পর্যন্ত আচরণটি রয়ে যায়। future-এর আরেক দুর্বলতা — কাজ জোড়া লাগানোর .then() নেই — মেটাতে Concurrency TS-এ then, when_all যোগ করা হয়েছিল, কিন্তু সেগুলো standard-এ ওঠেনি। কমিটি শেষে অন্য পথ বেছে নেয়: senders/receivers.

সমালোচনা থেকে শিক্ষাটি ব্যবহারিক: async খারাপ নয়, বরং ছোট “একবারের” সমান্তরাল হিসাবের জন্য খুব কাজের। শুধু তিনটি অভ্যাস রাখুন — policy স্পষ্ট লিখুন, future নামে রাখুন, আর অনেক ছোট কাজের জন্য এটি ব্যবহার করবেন না, কারণ প্রতিটি কাজে নতুন thread খরচসাপেক্ষ।

মূল উপলব্ধি: একটি API-র ডিফল্ট আচরণ যত অনিশ্চিত, তত বেশি বাগ জন্মায়। standard একবার প্রকাশ হলে আচরণ বদলানো প্রায় অসম্ভব, তাই C++ পুরোনো যন্ত্র না বদলে পাশে নতুন যন্ত্র যোগ করে — আর আপনার দায়িত্ব হলো পুরোনোটির ফাঁদগুলো জেনে রাখা।

প্র ০২C++11-এর memory model কেন দরকার হলো? Boehm ও Adve-এর ২০০৮ সালের কাজ “data race নেই তো ক্রমিক মনে হবে” নীতি দিয়ে কী সমাধান করল?

সমস্যার মূল হলো দুই পক্ষের চাওয়ার দ্বন্দ্ব। প্রোগ্রামার চান কোড ঠিক লেখা ক্রমে চলুক, সব thread একই ছবি দেখুক। compiler আর CPU চায় স্বাধীনতা — লেখা জমিয়ে রাখা, পড়া আগে করে ফেলা, ভ্যারিয়েবল register-এ রেখে দেওয়া — কারণ এসব ছাড়া আধুনিক প্রসেসর তার গতির বড় অংশ হারায়। এক thread-এ এসব পরিবর্তন অদৃশ্য, কিন্তু একাধিক thread থাকলে অন্য thread এই অদলবদল দেখে ফেলতে পারে। C++11-এর আগে কোনটি বৈধ আর কোনটি নয়, তা ভাষা বলত না; প্রতিটি compiler আর প্ল্যাটফর্মের নিজস্ব অলিখিত নিয়ম ছিল।

Hans Boehm-এর ২০০৫ সালের প্রবন্ধ দেখায়, সমস্যা শুধু লাইব্রেরি দিয়ে মেটে না — যেমন compiler পাশাপাশি দুটি ছোট ভ্যারিয়েবল একসাথে পড়ে-লিখে দিতে পারে, যা এক thread-এ নির্দোষ, কিন্তু অন্য thread-এর লেখা মুছে দেয়। এরপর Boehm আর Sarita Adve ২০০৮ সালে PLDI সম্মেলনে C++ memory model-এর ভিত্তি প্রকাশ করেন। মূল ধারণাটির নাম “SC for DRF” — data-race-free প্রোগ্রামের জন্য sequential consistency. সহজ ভাষায় চুক্তিটি: আপনি যদি data race না রাখেন (সব ভাগ করা ডেটা mutex বা ডিফল্ট atomic দিয়ে রক্ষিত), তাহলে প্রোগ্রাম এমনভাবে আচরণ করবে, যেন সব thread-এর কাজ কোনো একটি ক্রমিক ক্রমে মিশে চলছে। আর data race রাখলে undefined behavior — compiler-এর কোনো দায় নেই।

এই চুক্তি দুই পক্ষকেই রক্ষা করে। compiler race-মুক্ত অংশে স্বাধীনভাবে optimization করতে পারে, শুধু synchronization-এর জায়গায় (mutex, atomic) সংযত থাকতে হয়। প্রোগ্রামারকে CPU-র খুঁটিনাটি জানতে হয় না, শুধু race এড়াতে হয়। acquire/release আর relaxed হলো বিশেষজ্ঞদের জন্য রাখা দরজা, যেখানে ক্রমিক ছবির কিছুটা ছেড়ে দিয়ে গতি পাওয়া যায়। একই মডেল C11-এ গৃহীত হয়, আর Rust-সহ পরের অনেক ভাষা মূলত এটিই ধার নিয়েছে। মডেলটি নিখুঁত নয় — relaxed atomic-এর কিছু অদ্ভুত “out of thin air” ফল নিয়ে গবেষণা এখনো চলছে — তবে সাধারণ প্রোগ্রামারের জন্য চুক্তিটি স্পষ্ট।

মূল উপলব্ধি: memory model আসলে একটি চুক্তি: “race-মুক্ত কোড দাও, ক্রমিক আচরণ পাবে।” তাই আপনার প্রধান কাজ ordering-এর জাদু শেখা নয়, বরং প্রতিটি ভাগ করা ভ্যারিয়েবলের পাহারাদার ঠিক করা।

প্র ০৩Java-তে volatile দিয়ে thread-এর মাঝে পতাকা পাঠানো চলে, অথচ C++-এ চলে না। একই শব্দের মানে দুই ভাষায় এত আলাদা কেন?

C আর C++-এ volatile শব্দটি এসেছে hardware-এর সাথে কথা বলার প্রয়োজন থেকে। একটি microcontroller-এ কোনো ঠিকানা আসলে মেমরি নয় — একটি সেন্সরের register, যার মান বাইরের জগৎ বদলায়, বা যেখানে লিখলে একটি LED জ্বলে। compiler যদি ভাবে “এই মান তো কেউ বদলায়নি, আবার পড়ার দরকার নেই”, বা “পরপর দুবার লেখা, প্রথমটা বাদ দিই”, তাহলে hardware ঠিকমতো কাজ করবে না। volatile compiler-কে বলে: এই ভ্যারিয়েবলের প্রতিটি পড়া আর লেখা কোডে যেমন লেখা, ঠিক তেমনই করো। ব্যস, এটুকুই। এটি পড়া-বদলানো-লেখাকে atomic করে না, অন্য সাধারণ ভ্যারিয়েবলের সাথে ক্রমের কোনো নিশ্চয়তা দেয় না, আর CPU-কে কোনো barrier নির্দেশও পাঠায় না। দুই thread একটি volatile ভ্যারিয়েবলে লেখা-পড়া করলে C++-এ সেটি এখনো data race.

Java-র গল্প ভিন্ন। Java-র প্রথম memory model-এ গুরুতর ত্রুটি ছিল, আর ২০০৪ সালে Java 5-এর সাথে JSR-133 সেটি নতুন করে লেখে। সেখানে volatile-কে স্পষ্ট thread-অর্থ দেওয়া হয়: volatile ভ্যারিয়েবলে লেখা আর পড়া এমনভাবে সাজানো, যে লেখার আগের সব কাজ পরের পড়ার পরে দৃশ্যমান হয় — আমাদের acquire/release-এর মতো, আর সব volatile কাজ একটি সাধারণ ক্রমে। অর্থাৎ Java-র volatile আসলে C++-এর ডিফল্ট std::atomic-এর কাছাকাছি (যদিও Java-তে count++ volatile-এ তবু atomic নয়)। Java-তে hardware register-এর প্রশ্নই নেই, তাই শব্দটিকে নতুন অর্থ দেওয়া সহজ ছিল।

বিভ্রান্তি আরও বাড়িয়েছে Microsoft-এর compiler: x86-এ MSVC ঐতিহ্যগতভাবে volatile-কে acquire/release অর্থ দিত (/volatile:ms), আর ARM-এর জন্য ডিফল্ট standard আচরণ (/volatile:iso). ফলে Windows-এ পুরোনো কোড “চলত”, অন্য প্ল্যাটফর্মে ভাঙত। C++20 volatile-এর কিছু বিভ্রান্তিকর ব্যবহার — যেমন volatile ভ্যারিয়েবলে ++ — deprecated করেছে, যাতে কেউ এগুলোকে atomic ভেবে না বসে।

মূল উপলব্ধি: volatile মানে “এই মেমরি বাইরের জগতের সাথে যুক্ত, optimization কোরো না”; atomic মানে “এই মেমরি অন্য thread-এর সাথে ভাগ করা”। দুটি আলাদা প্রশ্নের উত্তর। একটি ভাষার শব্দের অর্থ অন্য ভাষায় ধরে নেওয়াই অনেক concurrency বাগের উৎস।

প্র ০৪lock-free stack-এ CAS ব্যবহার করলেও “ABA সমস্যা” হতে পারে। এটি কী, আর বাস্তবে কীভাবে ঠেকানো হয়?

CAS-এর প্রশ্নটা হলো “মান কি এখনো A?” — কিন্তু সে জানতে চায় না, মাঝখানে মান A থেকে অন্য কিছু হয়ে আবার A হয়েছে কি না। বেশিরভাগ ক্ষেত্রে তাতে কিছু যায় আসে না। কিন্তু pointer নিয়ে কাজ করলে যায় আসে। একটি lock-free stack ভাবুন, যার মাথায় node A, তার নিচে B. thread ১ pop করতে চায়: সে পড়ল মাথা = A, পরের = B, আর CAS করতে যাবে “মাথা এখনো A হলে B বসাও”। ঠিক তখন সে কিছুক্ষণের জন্য থেমে গেল। এর মধ্যে thread ২ A pop করল, B-ও pop করে মুছে দিল, তারপর একটি নতুন node push করল — আর মেমরি allocator সেই খালি হওয়া ঠিকানাটাই আবার দিল, অর্থাৎ নতুন node-এর ঠিকানাও A. এখন thread ১ জেগে CAS করে: মাথা কি A? হ্যাঁ! তাই সে মাথায় B বসায় — অথচ B অনেক আগেই মুছে গেছে। stack এখন একটি মুছে ফেলা মেমরির দিকে ইঙ্গিত করছে।

নামটা এসেছে সেই ক্রম থেকে: A, তারপর B, তারপর আবার A. ঠিকানা এক, কিন্তু বস্তু আলাদা। এর সাথে জড়িয়ে আছে আরও বড় প্রশ্ন — memory reclamation: lock-free কাঠামো থেকে সরানো node কখন নিরাপদে মোছা যাবে, যখন অন্য কোনো thread হয়তো এখনো সেটি পড়ছে?

প্রচলিত সমাধান কয়েকটি। এক, tagged pointer: pointer-এর সাথে একটি সংস্করণ নম্বর জুড়ে দেওয়া, যা প্রতিবার বদলায়; তখন CAS ঠিকানা আর সংস্করণ দুটোই মেলায়, তাই “আবার A” আর আগের A এক থাকে না। এর জন্য প্রায়ই দুই-word চওড়া CAS লাগে, যা সব CPU-তে নেই। দুই, hazard pointer — Maged Michael-এর ২০০৪ সালের কৌশল: প্রতিটি thread ঘোষণা করে “আমি এই node-টি ব্যবহার করছি”, আর কেউ ঘোষিত node মোছে না। তিন, epoch-ভিত্তিক বা RCU কৌশল, যা Linux kernel ব্যাপকভাবে ব্যবহার করে: পুরোনো node মোছা হয় তখনই, যখন নিশ্চিত যে সব পাঠক একটি “সময়-পর্ব” পার হয়েছে। C++26-এর জন্য hazard pointer আর RCU-ও standard লাইব্রেরিতে গৃহীত হয়েছে, যা দেখায় সমস্যাটি কতটা মৌলিক। আর garbage collector-যুক্ত ভাষায় সমস্যাটি অনেক হালকা, কারণ কেউ ধরে থাকলে ঠিকানা আবার ব্যবহার হয় না।

মূল উপলব্ধি: প্রতিটি atomic ধাপ সঠিক হলেও পুরো অ্যালগরিদম ভুল হতে পারে — কারণ “মান একই” আর “কিছু বদলায়নি” এক কথা নয়। এজন্যই lock-free কাঠামো নিজে না লিখে পরীক্ষিত লাইব্রেরি নেওয়াই বুদ্ধিমানের কাজ।

প্র ০৫senders/receivers (P2300, C++26-এর std::execution) future-এর চেয়ে কোন সমস্যা ভালো সমাধান করে, আর কেন এটি C++-এর async-এর ভবিষ্যৎ বলে ধরা হয়?

future মডেলের মূল সমস্যা তিনটি। প্রথমত, এটি আগ্রহী (eager): std::async ডাকামাত্র কাজ শুরু হয়, তাই কাজটি কোথায় চলবে বা কীভাবে বাতিল হবে, তা আগে থেকে বলার সুযোগ নেই। দ্বিতীয়ত, প্রতিটি future-এর জন্য heap-এ একটি shared state বরাদ্দ, তার ভেতরে তালা বা atomic — হাজারো ছোট কাজে খরচ জমে। তৃতীয়ত, জোড়া লাগানো যায় না: ফল পেয়ে পরের কাজ করতে হলে কাউকে get()-এ আটকে থাকতে হয়, অর্থাৎ একটি thread অলস বসে থাকে।

P2300 — Michał Dominiak, Lewis Baker, Lee Howes, Kirk Shoop, Michael Garland, Eric Niebler, Bryce Adelstein Lelbach-সহ কয়েকজনের প্রস্তাব — উল্টো দিক থেকে ভাবে। একটি sender হলো কাজের বর্ণনা, যা নিজে থেকে কিছু চালায় না। then, when_all, let_value-এর মতো algorithm দিয়ে sender জুড়ে বড় কাজের শৃঙ্খল বানানো হয়। একটি scheduler বলে কাজ কোথায় চলবে — CPU-র thread pool, GPU, বা একটি event loop. শৃঙ্খলটি শেষে একটি receiver-এর সাথে যুক্ত হলে তবেই চলে, আর ফলাফল তিন পথে আসে: মান, ভুল, বা বাতিল (stopped). যেহেতু পুরো শৃঙ্খলটি compile-এর সময় টাইপ হিসেবে জানা, compiler প্রায়ই সব কিছু inline করে দিতে পারে — heap বরাদ্দ বা তালা ছাড়াই।

এর পেছনে বাস্তব চাহিদা ছিল। Meta-র libunifex আর NVIDIA-র stdexec-এর মতো লাইব্রেরিতে এই ধারণা পরীক্ষিত হয়েছে; NVIDIA-র আগ্রহ ছিল একই C++ কোড দিয়ে CPU আর GPU দুই জায়গায় কাজ পাঠানো। আলোচনা কয়েক বছর চলেছে, নকশার জটিলতা নিয়ে তীব্র বিতর্কও হয়েছে, আর শেষে এটি C++26-এ গৃহীত হয়েছে। coroutine-এর সাথেও এটি মেলে: একটি sender-কে co_await করা যায়, ফলে async কোড প্রায় সাধারণ ক্রমিক কোডের মতো পড়া যায়।

তবে সতর্কতা: compiler আর লাইব্রেরির পূর্ণ সমর্থন আসতে সময় লাগবে, আর শেখার বাঁক খাড়া। আজকের প্রজেক্টে future, thread pool বা parallel algorithm-ই বাস্তব পছন্দ। কিন্তু আজ future-এর যেসব সীমা দেখলেন — জোড়া লাগানো যায় না, বাতিল করা যায় না, কোথায় চলবে বলা যায় না — সেগুলোই senders/receivers-এর নকশার উৎস।

মূল উপলব্ধি: “এখনই চালাও, পরে ফল দাও” (future) থেকে “আগে পুরো কাজের নকশা বানাও, তারপর ঠিক জায়গায় চালাও” (sender) — এটি async চিন্তার একটি বড় পরিবর্তন। future-এর দুর্বলতা বোঝা মানে নতুন নকশার কারণ বোঝা।

অনুশীলন

বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।

  1. আচরণ অনুমান: auto f = std::async(std::launch::deferred, work); তারপর while (f.wait_for(10ms) != std::future_status::ready) {} লেখা হলো। work কখন চলবে, আর লুপটি কী করবে?

    work কখনোই চলবে না, আর লুপ চিরকাল ঘুরবে। deferred কাজ শুরু হয় কেবল get() বা wait() ডাকলে; wait_for তাকে চালায় না, শুধু সাথে সাথে future_status::deferred ফেরত দেয় — যা কখনো ready নয়। policy না দিলেও একই ঘটনা ঘটতে পারে। সমাধান: std::launch::async দিন, অথবা লুপের আগে দেখুন অবস্থা deferred কি না, হলে সরাসরি get() ডাকুন।

  2. বাগ শিকার: একটি AI এই কোড দিয়েছে — volatile bool done = false; int counter = 0; চারটি thread লুপে counter++ করে, শেষে একটি thread done = true; লেখে, আর main while (!done) {} দিয়ে অপেক্ষা করে counter প্রিন্ট করে। কয়টি সমস্যা খুঁজে পান?

    অন্তত তিনটি। (১) counter++ চারটি thread থেকে — data race, গোনা হারায়, undefined behavior; std::atomic<int> আর fetch_add লাগবে। (২) volatile bool thread-এর পতাকা নয় — এটিও data race, আর এটি counter-এর সাথে কোনো ক্রম দেয় না; main পতাকা দেখেও পুরোনো counter পেতে পারে। std::atomic<bool> (ডিফল্ট বা release/acquire) লাগবে। (৩) “শেষে একটি thread done লেখে” — অন্য thread-গুলো শেষ হয়েছে কি না, তার নিশ্চয়তা নেই। সবচেয়ে সহজ ঠিক উপায়: পতাকাই বাদ দিন, সব thread join করে (বা jthread-এর scope শেষে) counter পড়ুন।

  3. সঠিক যন্ত্র বেছে নিন: কারণসহ বলুন — (ক) একটি ছবির thumbnail পেছনে বানিয়ে পরে দেখানো (খ) একটি দীর্ঘজীবী sensor thread প্রথম মাপটি পেয়ে main-কে জানাবে, তারপর মাপতে থাকবে (গ) অ্যাপের দশটি অংশ একই লগইন-তথ্যের জন্য অপেক্ষা করছে (ঘ) একটি API কতবার ডাকা হলো, তার গোনা (ঙ) ব্যাংক অ্যাকাউন্ট থেকে টাকা কমানো আর লেনদেনের ইতিহাসে লাইন যোগ — একসাথে।

    (ক) std::async(std::launch::async, ...) + future — একটি ফাংশনের একটি ফল। (খ) std::promise — ফল আসে কাজের মাঝপথে, thread চলতেই থাকে। (গ) std::shared_future — এক ফল, অনেক পাঠক, প্রত্যেকে নিজের কপি। (ঘ) std::atomic<long> আর fetch_add (relaxed যথেষ্ট, যদি শুধু গোনা হয়)। (ঙ) std::mutex — দুটি ডেটার মধ্যে নিয়ম রাখতে হবে, আলাদা দুটি atomic তা পারে না।

  4. নিজে লিখুন (কোড): উপরের async_sales.cpp-কে বদলান, যাতে টুকরোর সংখ্যা ৪-এর বদলে একটি ভ্যারিয়েবল k হয় (যেমন ৭), আর ভেক্টরের আকার k দিয়ে নিঃশেষে ভাগ না হলেও কোনো বিক্রি বাদ না পড়ে। যোগফল আগের মতোই আসা উচিত।

    মূল পরিবর্তন: step = sales.size() / k, আর শেষ টুকরোর শেষ সীমা (i + 1) * step নয়, বরং sales.size(). যেমন লুপের ভেতরে std::size_t to = (i == k - 1) ? sales.size() : (i + 1) * step;. দশ লাখকে ৭ দিয়ে ভাগ করলে ভাগশেষ ১ — শেষ টুকরোটি একটি বেশি উপাদান নেবে, যোগফল আবারও 50500000. k খুব বড় (ধরুন ১০০০) করলে আউটপুট একই থাকবে, কিন্তু প্রতিটি টুকরোর জন্য নতুন thread খোলার খরচে প্রোগ্রাম ধীর হতে পারে — অনুচ্ছেদ ৪-এর সতর্কতা মনে করুন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি loader thread একটি সাধারণ ভ্যারিয়েবলে ডেটা রাখে আর std::atomic<bool> ready দিয়ে UI thread-কে জানায়। নিরাপদ হস্তান্তরের ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. loader ডেটা লেখে
  2. loader ready.store(true, release) ডাকে
  3. UI ready.load(acquire) দিয়ে true দেখে
  4. UI ডেটা পড়ে — নিশ্চিতভাবে পূর্ণ মান

C++-এ auto f = std::async(std::launch::deferred, work); লেখা হলো। work কখন চলে?

  • সাথে সাথে, একটি নতুন thread-এ
  • প্রথমবার get() বা wait() ডাকলে, ডাকার thread-এই
  • প্রথমবার wait_for() ডাকলে, একটি নতুন thread-এ
  • future ধ্বংসের সময়, destructor-এর ভেতরে

C++-এ std::async(std::launch::async, f) দিয়ে চালানো f একটি std::runtime_error ছুড়ল। কী ঘটে?

  • সাথে সাথে std::terminate — পুরো প্রোগ্রাম বন্ধ
  • exception উপেক্ষিত হয়, get() একটি ডিফল্ট মান দেয়
  • exception জমা থাকে, get() ডাকলে সেটি আবার ছোড়া হয়
  • exception সাথে সাথে main thread-এ উড়ে আসে

C++-এ পরপর দুটি লাইন: std::async(std::launch::async, a); আর std::async(std::launch::async, b); — কোনো future রাখা হয়নি। a আর b কীভাবে চলে?

  • একটির পর একটি — a শেষ হলে তবে b শুরু
  • দুটো একসাথে, সমান্তরালে
  • কোনোটিই চলে না, কারণ future ফেলে দেওয়া হয়েছে
  • Compile error

C++-এ অনেক thread একটি website-এর hit সংখ্যা বাড়াবে। কোনটি নিরাপদ ও সঠিক?

  • volatile int hits; প্রতিটি thread-এ hits++
  • int hits; প্রতিটি thread-এ hits++
  • std::atomic<int> hits; hits.store(hits.load() + 1)
  • std::atomic<int> hits; hits.fetch_add(1)

C++-এ std::future<int> f-এ একবার f.get() সফলভাবে ডাকা হয়েছে। আবার f.get() ডাকলে কী হয়?

  • একই মান আবার ফেরত আসে
  • future আর valid নয় — undefined behavior (প্রায়ই future_error)
  • কাজটি আবার চলে, নতুন ফল আসে
  • thread চিরকাল আটকে থাকে

C++-এ একটি atomic ভ্যারিয়েবলে memory_order_relaxed দিয়ে কাজ করলে কী নিশ্চয়তা থাকে?

  • সব thread সব atomic কাজ একই ক্রমে দেখে
  • store-এর আগের সাধারণ লেখাগুলো অন্য thread-এ দেখা যাবে
  • শুধু ওই ভ্যারিয়েবলের কাজ atomic; অন্য ডেটার সাথে ক্রমের নিশ্চয়তা নেই
  • কোনো নিশ্চয়তা নেই — এটি সাধারণ ভ্যারিয়েবলের মতোই data race

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. std::async(std::launch::async, f) কাজ পেছনে চালায় আর একটি future (রসিদ) দেয়; policy না দিলে deferred-ও হতে পারে।
  2. get() ফল না আসা পর্যন্ত অপেক্ষা করে, একবারই ডাকা যায়, আর পেছনের exception এখানেই আবার ছোড়া হয়।
  3. async-এর future ফেলে দেবেন না — তার destructor অপেক্ষা করে, ফলে কাজ ক্রমিক হয়ে যায়। হাতে ফল পাঠাতে promise, পরে চালাতে packaged_task, অনেক পাঠকে shared_future.
  4. std::atomic একটি ভ্যারিয়েবলের জন্য: fetch_add, CAS লুপ, পতাকা। একাধিক ভ্যারিয়েবলের নিয়মে mutex. volatile কখনো thread-এর জন্য নয়।
  5. ডিফল্ট seq_cst রাখুন; “ডেটা রাখো, তারপর পতাকা তোলো”-তে release/acquire; relaxed শুধু খাঁটি counter-এ। নিজের lock-free কাঠামো নয় — আর ThreadSanitizer চালান।

পরের পর্বে: প্রোগ্রাম এখন কয়েকটি ফাইল, কয়েকটি লাইব্রেরি, thread-এর জন্য বিশেষ flag — হাতে g++ কমান্ড লিখে আর কুলায় না। CMake দিয়ে বড় প্রজেক্ট সাজানো, বিল্ড করা আর অন্য লাইব্রেরি যোগ করা।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান