এই পর্বে যা শিখবেন
- বাস্তবের concurrency বাগের শ্রেণিবিভাগ, আর atomicity ও order violation চিনে C-তে সারানো
- Mars Pathfinder-এর ঘটনা সময়রেখা ধরে; priority inheritance আর priority ceiling protocol-এর তুলনা, blocking-এর সীমার প্রমাণ আর হিসাব
- Lock convoy আর thundering herd; obstruction-free, lock-free আর wait-free-এর সংজ্ঞা আর Treiber stack
- ABA সমস্যার interleaving, আর tagged pointer, hazard pointer, epoch-based reclamation দিয়ে সমাধান
- Seqlock আর RCU; transactional memory-র ইতিহাস; actor, CSP, async/await; আর বাগ ধরার টুল
১ · বাস্তবের concurrency বাগ — একটি গবেষণার মানচিত্র
মডিউল ৪-এর আগের পর্বগুলোতে হাতিয়ারগুলো একে একে এসেছে: race condition আর Peterson (পর্ব ২২), atomic instruction আর memory ordering (পর্ব ২৩), mutex আর semaphore (পর্ব ২৪), condition variable (পর্ব ২৫), ক্লাসিক সমস্যা (পর্ব ২৬), আর deadlock ও Banker's algorithm (পর্ব ২৭–২৮)। মডিউলের এই শেষ পর্বে প্রশ্নটি উল্টো দিক থেকে: বাস্তবের সফটওয়্যারে মানুষ আসলে কোন ভুলগুলো করে, আর বড় সিস্টেম lock-এর সীমা পেরোতে কোন কৌশল নেয়?
একটি বিয়েবাড়ির রান্নাঘর ভাবুন। এক বাবুর্চি দেখলেন হাঁড়িতে লবণ নেই, লবণের কৌটা আনতে গেলেন; ফিরে এসে দিলেন — কিন্তু ততক্ষণে আরেকজন দিয়ে ফেলেছেন। এটি atomicity violation: “দেখা আর দেওয়া” একটানা হওয়ার কথা ছিল। আরেকদিন পরিবেশক চুলা জ্বলার আগেই খালি হাঁড়ি থেকে খাবার তুলতে গেলেন — order violation: রান্না আগে, পরিবেশন পরে, এই ক্রম কেউ নিশ্চিত করেনি। আর দুজন একজন হাতা ধরে আরেকজনের ডেকচির অপেক্ষায়, অন্যজন ডেকচি ধরে হাতার অপেক্ষায় — deadlock.
২০০৮ সালের ASPLOS সম্মেলনে Shan Lu, Soyeon Park, Eunsoo Seo আর Yuanyuan Zhou-এর একটি গবেষণাপত্র (“Learning from Mistakes”) MySQL, Apache, Mozilla আর OpenOffice থেকে বাস্তবে রিপোর্ট হওয়া ও সারানো ১০০-র কিছু বেশি concurrency বাগ বিশ্লেষণ করে। পেপারটির বহুল-উদ্ধৃত সারাংশ মোটামুটি এরকম (সংখ্যাগুলো ওই নমুনার, সব সফটওয়্যারের সাধারণ নিয়ম নয়):
| ধরন | কী ঘটে | গবেষণায় যা দেখা গেছে (মোটামুটি) | সাধারণ সমাধান |
|---|---|---|---|
| Atomicity violation | কয়েকটি ব্যবহার একসাথে অবিভাজ্য হওয়ার কথা, কিন্তু মাঝে অন্য থ্রেড ঢোকে | non-deadlock বাগের সবচেয়ে বড় অংশ | পুরো অংশটি একটি lock-এ, বা একটি atomic কাজে |
| Order violation | A-কে B-এর আগে হতে হবে, কিন্তু সেই ক্রম কেউ নিশ্চিত করেনি | দ্বিতীয় বড় অংশ; দুই দল মিলে non-deadlock বাগের প্রায় সবই | state flag + condition variable, join, semaphore |
| Deadlock | থ্রেডেরা চক্রাকারে একে অপরের সম্পদের অপেক্ষায় | মোট বাগের প্রায় তিন ভাগের এক ভাগ; প্রায় সবগুলোতে দুটি বা তার কম সম্পদ | lock-এর নির্দিষ্ট ক্রম, timeout, try-lock (পর্ব ২৭) |
পেপারের আরেকটি পর্যবেক্ষণ খুব কাজের: অধিকাংশ বাগ ঘটাতে দুটি থ্রেডই যথেষ্ট, আর তাদের কয়েকটি নির্দিষ্ট ব্যবহারের ক্রম ঠিক করে দিলেই বাগটি নিশ্চিতভাবে দেখা দেয়। তাই “দুই থ্রেড, কয়েকটি বিন্দু” ধরে সময়-বিন্যাস খুঁজলেই বেশিরভাগ বাগ ধরা সম্ভব — অংশ ৮-এর টুলগুলো এই ধারণার ওপর দাঁড়িয়ে।
Atomicity violation — যাচাই আর ব্যবহারের মাঝে ফাঁক
পেপারে উদ্ধৃত MySQL-এর একটি বাগের সরল রূপ। থ্রেড ১ দেখে একটি pointer খালি কিনা, তারপর সেটি ব্যবহার করে; থ্রেড ২ মাঝখানে pointer-টিকে NULL করে দেয়। একে বলে check-then-act বাগ:
/* BUGGY: check and use are two separate steps */
/* Thread 1 */ /* Thread 2 */
if (thd->proc_info) {
thd->proc_info = NULL;
fputs(thd->proc_info, out); /* NULL! crash */
}
/* FIXED: the check and the use happen under one lock */
pthread_mutex_lock(&info_lock); /* Thread 2 also takes info_lock */
if (thd->proc_info) /* before it sets proc_info = NULL */
fputs(thd->proc_info, out);
pthread_mutex_unlock(&info_lock);লক্ষ করুন, এখানে প্রতিটি আলাদা পড়া বা লেখা হয়তো নিজে atomic — pointer পড়া x86-64-এ একটিই instruction. বাগটি কোনো একক ব্যবহারে নয়, দুটি ব্যবহারের মাঝের ফাঁকে। প্রোগ্রামার ধরে নিয়েছিলেন যে “যাচাই করে ব্যবহার” একটানা হবে। তাই শুধু ভ্যারিয়েবলটিকে _Atomic করলে বাগ সারে না; পুরো অংশটিকেই একটি critical section বানাতে হয়, অথবা মানটি একবার local-এ পড়ে সেটিই যাচাই আর ব্যবহার করতে হয় (তখন মেমরিটি যেন মুক্ত না হয়, তা আলাদাভাবে নিশ্চিত করতে হয় — অংশ ৫ আর ৬)। ব্যালান্স দেখে টাকা তোলাও একই রোগ — DBMS কোর্সের concurrency control-এ এটিই lost update আর write skew.
Order violation — “আগে init, পরে ব্যবহার” কেউ নিশ্চিত করেনি
পেপারে উদ্ধৃত Mozilla-র একটি বাগের ধাঁচে: main থ্রেড একটি worker তৈরি করে, তারপর একটি shared pointer-এ কনফিগারেশন বসায়। লেখকের মাথায় ছিল “worker চলতে চলতে তো init হয়ে যাবে”। কিন্তু pthread_create ফেরার আগেই worker চলতে পারে। নিচের প্রোগ্রামে অনিশ্চয়তা দূর করতে একটি flag দিয়ে দুর্ভাগ্যজনক ক্রমটি জোর করে ঘটানো হয়েছে — যাতে প্রতিবার একই ফল আসে। দ্বিতীয় অংশে একই কাজ সঠিকভাবে: worker একটি state flag-এর জন্য condition variable-এ অপেক্ষা করে:
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
struct config { int port; };
static struct config real = { 8080 };
/* BUGGY: the worker assumes main has already set cfg */
static _Atomic(struct config *) cfg = NULL;
static atomic_int checked = 0; /* only to force the unlucky order */
static void *buggy_worker(void *arg) {
(void)arg;
struct config *c = atomic_load(&cfg);
printf("buggy: worker sees cfg = %s\n", c ? "set" : "NULL (c->port would crash)");
atomic_store(&checked, 1);
return NULL;
}
/* FIXED: the worker waits for a state flag that main sets after init */
static struct config *ready = NULL;
static pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
static void *fixed_worker(void *arg) {
(void)arg;
pthread_mutex_lock(&mu);
while (ready == NULL) pthread_cond_wait(&cv, &mu); /* not ready: sleep */
int port = ready->port;
pthread_mutex_unlock(&mu);
printf("fixed: worker uses port %d\n", port);
return NULL;
}
int main(void) {
pthread_t t;
pthread_create(&t, NULL, buggy_worker, NULL);
while (!atomic_load(&checked)) { } /* force: worker runs before init */
atomic_store(&cfg, &real); /* init comes too late */
pthread_join(t, NULL);
pthread_create(&t, NULL, fixed_worker, NULL); /* worker may run first, */
pthread_mutex_lock(&mu);
ready = ℜ /* but it cannot use cfg */
pthread_cond_signal(&cv); /* before this point */
pthread_mutex_unlock(&mu);
pthread_join(t, NULL);
return 0;
}একটি আসল রানের আউটপুট:
buggy: worker sees cfg = NULL (c->port would crash)
fixed: worker uses port 8080
প্রথম লাইনে worker দেখেছে cfg এখনো NULL — আসল প্রোগ্রামে এখানেই c->port পড়তে গিয়ে segmentation fault হতো। এখানে ক্রমটি checked flag দিয়ে জোর করা, কিন্তু বাস্তবে ঠিক এই ক্রম হাজারে একবার আপনাআপনি ঘটে — বিশেষ করে ব্যস্ত সার্ভারে বা core কম এমন container-এ। দ্বিতীয় লাইনে worker আগে চলুক বা পরে, সে ready না দেখা পর্যন্ত ঘুমায়; আর ready বসানো হয় mutex ধরে, init শেষ হওয়ার পরে। তাই ফল প্রতিবার 8080. এখানে state flag (ready) আসল কাজ করছে, condition variable শুধু ঘুম আর জাগানোর ব্যবস্থা — signal আগে চলে গেলেও worker flag দেখে আর ঘুমায় না। মনে রাখার নিয়ম: ক্রমের প্রতিটি অনুমান কোডে একটি সমন্বয় হয়ে থাকবে — join, semaphore, বা flag + condition variable.
২ · Priority inversion গভীরে — Mars Pathfinder-এর সময়রেখা
পর্ব ১৮-এ (Priority scheduling আর Round Robin) priority inversion-এর মূল ছবি দেখেছেন: উচ্চ task H একটি lock-এর জন্য নিম্ন task L-এর অপেক্ষায়, আর মাঝারি task M, যার lock-এর সাথে কোনো সম্পর্কই নেই, L-কে সরিয়ে রাখে। এবার সংখ্যা দিয়ে, সময়সীমা দিয়ে, আর একটি আসল মহাকাশযানের গল্প দিয়ে।
১৯৯৭ সালের ৪ জুলাই NASA-র Mars Pathfinder মঙ্গলে নামে। কিছুদিন পর এর কম্পিউটার মাঝে মাঝে নিজে থেকে পুরো রিসেট হতে থাকে, আর প্রতিবার সেদিনের কিছু তথ্য হারায়। প্রকল্পের সফটওয়্যার দলের প্রধান Glenn Reeves-এর পরে দেওয়া বহুল-উদ্ধৃত বিবরণ অনুযায়ী, যানটির কম্পিউটার চলত Wind River-এর VxWorks real-time OS-এ। একটি উচ্চ-অগ্রাধিকারের bus management task (একটি ভাগের “information bus”-এ তথ্য আদান-প্রদান সামলাত) নিয়মিত একটি mutex নিত। একটি নিম্ন-অগ্রাধিকারের meteorological (আবহাওয়া-তথ্য সংগ্রহের) task-ও মাঝে মাঝে একই mutex নিয়ে bus-এ তথ্য প্রকাশ করত। আর মাঝারি অগ্রাধিকারের, তুলনামূলক দীর্ঘক্ষণ চলা communications task-এর ওই mutex লাগত না। দুর্ভাগ্যজনক মুহূর্তে মাঝারি task নিম্ন task-কে সরিয়ে রাখত, উচ্চ task সময়মতো শেষ হতো না, আর একটি watchdog ব্যবস্থা তা দেখে “কিছু একটা ভীষণ ভুল” ধরে নিয়ে পুরো সিস্টেম রিসেট করত। পৃথিবীতে একই রকম একটি যন্ত্রে ঘটনাটি পুনরায় ঘটিয়ে দেখার পর, সংশ্লিষ্ট mutex-এ VxWorks-এর priority inheritance বিকল্পটি চালু করার একটি ছোট পরিবর্তন যানে পাঠানো হয়েছিল, আর রিসেট বন্ধ হয় — বিবরণগুলো এমনটাই জানায়।
নিচের ডায়াগ্রামে সংখ্যাগুলো শেখার জন্য বানানো (মহাকাশযানের আসল সময় নয়), কিন্তু কাঠামো একই। সময় এককে মাপা। L-এর critical section ৩ একক, যার শুরুতেই সে lock R নেয়। H আসে t = 1-এ; তার মোট কাজ ৩ একক — ১ একক নিজের হিসাব, তারপর R নিয়ে ২ একক। Watchdog-এর নিয়ম: H-কে t = 8-এর মধ্যে শেষ করতে হবে। M আসে t = 3-এ, ৬ একক কাজ নিয়ে, R লাগে না। অগ্রাধিকার H > M > L, আর scheduler preemptive.
| সময় | |
| CPU-তে | |
| H (bus management) | |
| M (communications) | |
| L (meteorological) | |
| R-এর মালিক |
- t = 0: L lock R নেয়। শুধু L ready, তাই সে চলে আর critical section-এর শুরুতে R নেয়। তার CS মোট ৩ একক।
- t = 1: H জাগে, L-কে সরায়। H-এর অগ্রাধিকার সবচেয়ে বেশি, তাই preemptive scheduler সঙ্গে সঙ্গে L-কে সরিয়ে H-কে চালায়। H ১ একক নিজের হিসাব করে। L-এর CS-এর ১ একক হয়েছে, ২ একক বাকি।
- t = 2: H-এর R দরকার — block. R L-এর হাতে, তাই H ঘুমিয়ে পড়ে। L আবার চলে। এই অপেক্ষাটুকু স্বাভাবিক ও সীমিত: বড়জোর L-এর বাকি CS, অর্থাৎ ২ একক। একে বলে bounded (সীমিত) blocking.
- t = 3: M জাগে। M > L, আর H blocked — ফলে ready task-দের মধ্যে M-ই সবচেয়ে উঁচু। M L-কে সরিয়ে দেয়। L-এর CS-এর তখনো ১ একক বাকি, কিন্তু CPU না পেলে সে R ছাড়তে পারবে না।
- t = 3 থেকে 8: M চলতেই থাকে। H কার্যত অপেক্ষা করছে M-এর জন্য, যদিও M-এর সাথে R-এর কোনো সম্পর্ক নেই। এটিই unbounded priority inversion: H-এর অপেক্ষা আর L-এর CS-এর দৈর্ঘ্যে সীমিত নয়, বরং যত মাঝারি task আসবে, তত বাড়বে।
- t = 8: watchdog রিসেট করে। H এখনো ২ একক কাজ বাকি নিয়ে আটকে। Pathfinder-এ ঠিক এই মুহূর্তে watchdog পুরো কম্পিউটার রিসেট করত। কোনো কোড “ক্র্যাশ” করেনি — প্রতিটি task নিজের নিয়মে ঠিকঠাক চলেছে; ভুলটি ছিল নিয়মগুলোর মিলনে।
- Inheritance সহ, t = 2 থেকে 4. একই শুরু, কিন্তু H যখন R-এর জন্য block হয়, kernel R-এর মালিক L-কে সাময়িকভাবে H-এর অগ্রাধিকার দেয়। t = 3-এ M জাগে, কিন্তু L-এর কার্যকর অগ্রাধিকার এখন M-এর চেয়ে বেশি, তাই M অপেক্ষা করে। L বাকি ২ একক CS শেষ করে t = 4-এ R ছাড়ে, আর সঙ্গে সঙ্গে নিজের নিম্ন অগ্রাধিকারে ফেরে।
- t = 4: H R পায়, t = 6-এ শেষ। H বাকি ২ একক চলে আর t = 6-এ শেষ — সময়সীমা ৮-এর আগেই। তারপর M t = 6 থেকে ১২ পর্যন্ত চলে। H-এর মোট blocking ছিল ২ একক (t = 2 থেকে 4), ঠিক L-এর বাকি CS-এর সমান — M-এর ৬ একক এখন আর H-কে ছোঁয় না।
কোনো একটি task-এর কোডে ভুল ছিল না; বাগটি ছিল তিনটি নীতির সংঘর্ষে — preemptive priority scheduling, একটি ভাগের mutex, আর একটি দীর্ঘ মাঝারি task (প্র ০১-এ আরও)।
৩ · Priority inheritance বনাম priority ceiling — প্রমাণ, হিসাব আর lock-এর আরও দুই রোগ
Priority inversion-এর আনুষ্ঠানিক তত্ত্ব দেন Lui Sha, Ragunathan Rajkumar আর John Lehoczky, ১৯৯০ সালে IEEE Transactions on Computers-এ প্রকাশিত “Priority Inheritance Protocols: An Approach to Real-Time Synchronization” প্রবন্ধে। সেখানে দুটি protocol আছে।
Priority Inheritance Protocol (PIP)
উচ্চ task যখন একটি lock-এর জন্য block হয়, lock-এর মালিক সেই task-এর অগ্রাধিকার উত্তরাধিকার পায়, lock ছাড়লে আগের অগ্রাধিকারে ফেরে। সহজ, শুধু দরকারের সময় সক্রিয়। কিন্তু nested lock থাকলে একটি task বারবার আটকাতে পারে (chained blocking), আর deadlock ঠেকায় না।
Priority Ceiling Protocol (PCP)
প্রতিটি lock-এর একটি ceiling = যে task-গুলো এটি কখনো নিতে পারে, তাদের সর্বোচ্চ অগ্রাধিকার। একটি task নতুন lock পায় শুধু যদি তার অগ্রাধিকার অন্যদের হাতে থাকা সব lock-এর সর্বোচ্চ ceiling-এর চেয়ে বেশি হয়। ফল: সর্বোচ্চ এক critical section-এর blocking, আর deadlock অসম্ভব। দাম: কোন task কোন lock নেবে, আগে থেকে জানতে হয়।
Immediate ceiling (highest locker)
PCP-র সরল রূপ: lock নেওয়ার মুহূর্তেই মালিকের অগ্রাধিকার lock-এর ceiling-এ তোলা হয়। POSIX-এর PTHREAD_PRIO_PROTECT এভাবে কাজ করে; Ada-র protected object-এর ceiling locking-ও এই ধাঁচের।
কেন PCP-তে blocking সর্বোচ্চ একটি critical section — একটি ছোট প্রমাণ
এক প্রসেসরের সিস্টেম ধরুন। “L, H-কে আটকাতে পারে” মানে L এমন একটি lock ধরে আছে, যার ceiling ≥ H-এর অগ্রাধিকার। দাবি: H-কে বড়জোর একটি নিম্ন task-এর একটি critical section আটকায়।
- ধাপ ১: ধরুন H আসার সময় দুটি নিম্ন task, L1 আর L2, এমন lock ধরে আছে যা H-কে আটকাতে পারে — L2-র হাতে S2, L1-এর হাতে S1, আর L2 আগে S2 নিয়েছে। তাহলে L1 যখন পরে S1 নিল, তখন S2 অন্যের হাতে ছিল, তাই PCP-র নিয়মে L1-এর অগ্রাধিকার ceiling(S2)-এর চেয়ে বেশি হতে হয়েছে। কিন্তু ceiling(S2) ≥ H-এর অগ্রাধিকার > L1-এর অগ্রাধিকার — বিরোধ। তাই এমন task বড়জোর একটি।
- ধাপ ২: H ready হওয়ার পর কোনো নিম্ন task নতুন করে এমন lock নিতে পারে না, কারণ H ready থাকতে নিম্ন task চলে শুধু উত্তরাধিকার পাওয়া অগ্রাধিকারে, তার বর্তমান critical section শেষ করার জন্য।
- ফল: H-এর blocking ≤ একটি নিম্ন task-এর দীর্ঘতম critical section (যার lock-এর ceiling ≥ H). একই যুক্তিতে deadlock-এর চক্রও অসম্ভব: চক্রের দ্বিতীয় task তার প্রথম lock-টিই নিতে পারত না। ∎
PIP-এ এই নিশ্চয়তা নেই: সেখানে H প্রতিটি নিম্ন task-এর কাছে, বা প্রতিটি lock-এর কাছে, বড়জোর একবার করে আটকাতে পারে — অর্থাৎ সীমা min(n, m)টি critical section, যেখানে n নিম্ন task-এর সংখ্যা আর m lock-এর সংখ্যা।
একটি পূর্ণ হিসাব
চারটি task, অগ্রাধিকার τ1 > τ2 > τ3 > τ4. দুটি lock S1, S2. τ1 দুটিই ব্যবহার করে। τ3 S1 ধরে রাখে সর্বোচ্চ ৪ ms, τ4 S2 ধরে রাখে সর্বোচ্চ ৩ ms. τ2 কোনো lock নেয় না। τ1-এর worst-case blocking কত?
- Ceiling: S1 আর S2 দুটিই τ1 নেয়, তাই ceiling(S1) = ceiling(S2) = τ1-এর অগ্রাধিকার।
- PIP: দুর্ভাগ্যজনক ক্রমে τ4 S2 নিয়ে আছে, তাকে সরিয়ে τ3 S1 নিল; তারপর τ1 এল। τ1 প্রথমে S1-এ আটকায় (τ3-এর ৪ ms), তারপর S2-তে (τ4-এর ৩ ms). B1 = ৪ + ৩ = ৭ ms.
- PCP: τ4 S2 ধরে থাকা অবস্থায় τ3 S1 নিতে পারত না, কারণ τ3-এর অগ্রাধিকার ceiling(S2)-এর চেয়ে বেশি নয়। তাই একসাথে দুজন হাতে থাকা সম্ভব নয়; B1 = max(৪, ৩) = ৪ ms.
Real-time সিস্টেমে এই B মান পর্ব ২০-এর response-time হিসাবে যোগ হয়: R = C + B + (উচ্চতর task-দের interference). তাই blocking-এর সীমা না জানলে schedulability প্রমাণ করা যায় না।
বাস্তবের OS-এ
pthread_mutexattr_t a;
pthread_mutex_t m;
pthread_mutexattr_init(&a);
int rc = pthread_mutexattr_setprotocol(&a, PTHREAD_PRIO_INHERIT); /* or PTHREAD_PRIO_PROTECT */
if (rc != 0) fprintf(stderr, "setprotocol: %s\n", strerror(rc));
/* for PRIO_PROTECT also: pthread_mutexattr_setprioceiling(&a, ceiling); */
pthread_mutex_init(&m, &a);
pthread_mutexattr_destroy(&a);Linux-এ PTHREAD_PRIO_INHERIT mutex glibc বাস্তবায়ন করে PI-futex (FUTEX_LOCK_PI) দিয়ে, যা ২০০৬ সালের দিকে 2.6.18 kernel-এ এসেছিল; kernel-এর ভেতরে একই কাজ করে rt_mutex, আর PREEMPT_RT-এ সাধারণ spinlock-ও rt_mutex হয়ে যায়। Inheritance কাজে আসে শুধু real-time নীতিতে (SCHED_FIFO/SCHED_RR) চলা task-এ — CFS/EEVDF-এর nice মান কঠোর অগ্রাধিকার নয় (পর্ব ২১)। Windows-এর সাধারণ mutex-এ POSIX-ধাঁচের inheritance নেই; Windows Internals বইয়ের বর্ণনা অনুযায়ী Windows অনাহারী ready thread-কে সাময়িক boost দিয়ে সমস্যাটি কমায়। macOS-এ Apple-এর ডকুমেন্টেশন অনুযায়ী os_unfair_lock মালিকের তথ্য রাখে বলে kernel অগ্রাধিকার inversion এড়াতে পারে।
Lock-এর আরও দুই রোগ — convoy আর thundering herd
Lock convoy: একটি জনপ্রিয় lock-এর পেছনে অনেক থ্রেড সারি বাঁধে। Lock যদি “ন্যায্য হস্তান্তর” করে — ছাড়ার সময় সারির প্রথমজনকে দিয়ে দেয় — তাহলে প্রতিটি হস্তান্তরে একটি ঘুমন্ত থ্রেড জাগাতে হয়, আর জাগতে জাগতে lock অলস পড়ে থাকে। ফলে lock-এর critical section মাত্র কয়েক মাইক্রোসেকেন্ডের হলেও throughput নামে context switch-এর গতিতে, আর সারিটি নিজে থেকে আর ভাঙে না — যেমন অফিস ছুটির সময় ঢাকার কোনো মোড়ে সিগন্যাল সবুজ হলেও প্রতিটি গাড়ি চালু হতে সময় নেয়, তাই সারি ছোট হয় না। ১৯৭০-এর দশকের শেষে IBM-এর System R ডেটাবেসের প্রকৌশলীরা এটিকে “convoy phenomenon” নামে বর্ণনা করেছিলেন। প্রতিকার: ছোট critical section, lock ভাগ করা (প্রতিটি bucket-এর নিজের lock), আর “barging” — চলমান থ্রেড ঘুমন্তকে টপকে lock নিতে পারে; glibc আর Linux kernel-এর mutex মোটামুটি এ পথেই চলে।
Thundering herd: একটি ঘটনার জন্য অনেক থ্রেড অপেক্ষায়, ঘটনা ঘটলে সবাইকে জাগানো হয়, কিন্তু কাজ পায় একজনই — বাকিরা CPU খরচ করে আবার ঘুমায়। যেমন একটি টোকেন ডাকলে ব্যাংকের সব গ্রাহক একসাথে কাউন্টারে ছুটে আসা। উদাহরণ: একটি আইটেমের জন্য pthread_cond_broadcast (যেখানে signal-ই যথেষ্ট), বা অনেক প্রসেস একই listening socket-এ অপেক্ষা। Linux বহু আগেই blocking accept()-এ একজনকেই জাগায়; epoll-এর জন্য Linux 4.5-এ EPOLLEXCLUSIVE যোগ হয়েছে, আর SO_REUSEPORT সংযোগগুলো আলাদা socket-এ ভাগ করে দেয়। তবে shutdown-এর মতো জায়গায় broadcast-ই ঠিক (পর্ব ১৫)।
৪ · Lock-free আর wait-free — progress-এর তিন স্তর আর Treiber stack
Lock-এর মৌলিক দুর্বলতা: lock-ধারী থ্রেড থেমে গেলে (preempt, page fault, মৃত্যু) বাকি সবাই আটকে থাকে — inversion, convoy আর deadlock এরই রূপ। বিকল্প: এমন ডেটা স্ট্রাকচার, যেখানে কেউ অন্যকে আটকে রাখতেই পারে না। এদের নিশ্চয়তা মাপা হয় progress guarantee দিয়ে:
| স্তর | নিশ্চয়তা | কে আটকে যেতে পারে |
|---|---|---|
| Blocking (lock-ভিত্তিক) | কিছুই না, যদি lock-ধারী থেমে যায় | সবাই |
| Obstruction-free | একটি থ্রেডকে একা (অন্যরা থামা অবস্থায়) চলতে দিলে সে সসীম ধাপে শেষ করবে | একসাথে চললে সবাই বারবার একে অপরকে বাতিল করতে পারে (livelock) |
| Lock-free | যেকোনো সময়ে, সসীম ধাপের মধ্যে কোনো না কোনো থ্রেডের কাজ শেষ হবে — সিস্টেম হিসেবে অগ্রগতি | একটি নির্দিষ্ট থ্রেড চিরকাল হারতে পারে (starvation) |
| Wait-free | প্রতিটি থ্রেড নিজের কাজ সসীম ধাপে শেষ করবে, অন্যরা যা-ই করুক | কেউ না |
প্রতিটি স্তর আগেরটির চেয়ে শক্তিশালী। Maurice Herlihy-র ১৯৯১ সালের “Wait-Free Synchronization” প্রবন্ধ দেখায় যে শুধু read/write দিয়ে সাধারণ wait-free ডেটা স্ট্রাকচার বানানো যায় না, কিন্তু compare-and-swap (CAS)-এর মতো instruction দিয়ে যায় — এ কারণেই আধুনিক CPU-গুলোতে CAS (x86-এর lock cmpxchg) বা LL/SC (ARM, POWER, RISC-V) থাকে (পর্ব ২৩)। CAS-এর অর্থ এক লাইনে: ঠিকানাটিতে যদি এখনো আগে দেখা মানটিই থাকে, তবে নতুন মান বসানো হয় আর ফল “সফল”; নইলে কিছুই বদলায় না, আর এখনকার মানটি জানিয়ে দেওয়া হয় — পুরোটা একটি অবিভাজ্য ধাপে।
Treiber stack — একটি pointer, একটি CAS
R. Kent Treiber ১৯৮৬ সালে IBM-এর একটি গবেষণা প্রতিবেদনে সবচেয়ে সরল lock-free ডেটা স্ট্রাকচারগুলোর একটি বর্ণনা করেন: একটি linked-list stack, যার পুরো অবস্থা একটি top pointer. Push: নতুন নোডের next-এ বর্তমান top বসানো, তারপর CAS(top, দেখা top, নতুন নোড). Pop: top পড়া, তার next পড়া, তারপর CAS(top, দেখা top, next). কেউ মাঝে top বদলে দিলে CAS ব্যর্থ হয়, আর আবার চেষ্টা। (Linked list আর stack-এর মৌলিক ধারণা: DSA কোর্সের stack পর্ব)
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
struct node { int val; struct node *next; };
static _Atomic(struct node *) top = NULL; /* whole stack = one pointer */
static void push(struct node *n) {
struct node *old = atomic_load(&top);
do n->next = old; /* link to the top we saw */
while (!atomic_compare_exchange_weak(&top, &old, n)); /* moved? retry */
}
static struct node *pop(void) {
struct node *old = atomic_load(&top);
while (old && !atomic_compare_exchange_weak(&top, &old, old->next)) { }
return old; /* on CAS failure, old = the new top */
}
static void *pusher(void *arg) {
int base = *(int *)arg;
for (int i = 0; i < 1000; i++) {
struct node *n = malloc(sizeof *n);
if (!n) abort();
n->val = base + i; push(n);
}
return NULL;
}
int main(void) {
pthread_t a, b; int base_a = 0, base_b = 1000;
pthread_create(&a, NULL, pusher, &base_a); /* pushes 0..999 */
pthread_create(&b, NULL, pusher, &base_b); /* pushes 1000..1999 */
pthread_join(a, NULL); pthread_join(b, NULL);
static char seen[2000]; int count = 0, dup = 0; long sum = 0;
for (struct node *n; (n = pop()) != NULL; free(n)) { /* one thread: no ABA */
count++; sum += n->val; dup += seen[n->val]++ ? 1 : 0;
}
printf("lock-free: %s\n", atomic_is_lock_free(&top) ? "yes" : "no");
printf("popped %d nodes, sum %ld (expected %ld), duplicates %d\n",
count, sum, 1999L * 2000 / 2, dup);
return 0;
}একটি আসল রানের আউটপুট:
lock-free: yes
popped 2000 nodes, sum 1999000 (expected 1999000), duplicates 0
দুই থ্রেড একসাথে push করেছে, কোনো lock নেই, তবু ঠিক ২০০০টি নোড, কোনোটি দুবার নয়, আর যোগফল ০ + ১ + … + ১৯৯৯ = ১৯৯৯ × ২০০০ ÷ ২ = ১৯,৯৯,০০০. দুই থ্রেডের নোড stack-এ কোন ক্রমে মিশেছে, তা প্রতিবার আলাদা — কিন্তু গোনা, যোগফল আর “duplicates 0” ক্রমের ওপর নির্ভর করে না, তাই আউটপুট প্রতিবার এক। atomic_is_lock_free জানায় যে এই প্ল্যাটফর্মে pointer-এর atomic কাজ আসল CPU instruction দিয়ে হয়, ভেতরে লুকানো lock দিয়ে নয়। CAS ব্যর্থ হলে atomic_compare_exchange_weak নিজেই old-এ নতুন top লিখে দেয়, তাই লুপে আলাদা করে পড়তে হয় না; weak সংস্করণ মাঝে মাঝে বিনা কারণে ব্যর্থ হতে পারে (LL/SC-ওয়ালা CPU-তে), সেজন্যই এটি লুপে।
এখানে ABA কেন ঘটেনি? একসাথে চলার সময় শুধু push হয়েছে। Push-এর CAS-এ যদি top কোনোভাবে আগের মানে ফিরেও আসে, n->next = old তখনো সঠিক — তাই push ABA-তে ভাঙে না। ঝুঁকি pop-এ, যে CAS-এর আগে old->next পড়ে রাখে। আর এখানে pop হয়েছে join-এর পরে, একটি থ্রেড থেকে; চলার সময় কোনো নোড মুক্ত বা পুনর্ব্যবহারও হয়নি। দুই থ্রেড একসাথে pop আর free করলে কী হয়, পরের অংশে।
প্রমাণ: Treiber stack lock-free, কিন্তু wait-free নয়
- পর্যবেক্ষণ:
top-এ লেখে শুধু সফল CAS. একটি থ্রেড T-এর CAS ব্যর্থ হয় শুধু যদি T-এর পড়া আর CAS-এর মাঝেtopবদলেছে (weak CAS-এর কারণহীন ব্যর্থতা বাদে, যা hardware সীমিত রাখে)। - তাই: T-এর প্রতিটি “আসল” ব্যর্থতার পেছনে অন্য কোনো থ্রেডের একটি সফল CAS আছে — অর্থাৎ কারও একটি push বা pop সম্পূর্ণ হয়েছে। T যদি অসীমবার ব্যর্থ হয়, অন্যরা অসীমবার সফল হয়েছে। সিস্টেম কখনো থামে না — এটিই lock-free-এর সংজ্ঞা। কেউ মাঝপথে থেমে গেলেও (preempt বা মৃত্যু) সে কোনো lock ধরে রাখে না, তাই কেউ আটকায় না।
- Wait-free নয়: দুর্ভাগ্যজনক scheduler-এ T প্রতিবার পড়ার পর অন্য কেউ top বদলে দিতে পারে — T-এর ধাপের কোনো ঊর্ধ্বসীমা নেই। ∎
তাই lock-free কাঠামো priority inversion থেকে মুক্ত; তবে hard real-time-এ প্রতিটি task-এর পূর্ণ নিশ্চয়তা দেয় শুধু wait-free.
৫ · ABA সমস্যা — CAS যখন ভুল করে সফল হয়
CAS শুধু যাচাই করে “মানটি কি এখনো একই?” — “এর মধ্যে কি কিছু ঘটেছে?” নয়। ধরুন আপনি বাড়ির দারোয়ানকে বললেন, “গেটের সামনে সাদা গাড়িটি থাকলে ড্রাইভারকে চাবি দিন।” আপনি সরে যাওয়ার পর গাড়িটি চলে গেল, আর একই মডেলের আরেকটি সাদা গাড়ি এসে দাঁড়াল। দারোয়ান দেখলেন “সাদা গাড়ি আছে” — শর্ত মিলেছে — আর ভুল মানুষকে চাবি দিলেন। মান A থেকে B হয়ে আবার A হলে CAS তফাত বোঝে না; একেই বলে ABA সমস্যা. Lock-free stack-এ এটি ঘটে যখন একটি নোড মুক্ত হয়ে আবার একই ঠিকানায় ফিরে আসে — malloc প্রায়ই সদ্য মুক্ত ঠিকানাই আবার দেয়।
| top | |
| stack (top থেকে) | |
| T1-এর local | |
| T2 যা করল | |
| নোড B | |
| version (tag) |
- শুরু। Stack-এ তিনটি নোড: top → A → B → C. দুটি থ্রেড একসাথে pop করবে।
- T1 pop শুরু করে, থেমে যায়। T1 পড়ে
old = AআরA->next = B, তারপর CAS-এর ঠিক আগে preempt হয়। এখন T1-এর হাতে একটি “ছবি”: top A, তার পরে B. - T2 A-কে pop করে। T2-এর CAS(A → B) সফল; top = B. T1 এখনো ঘুমিয়ে, তার ছবি পুরোনো হয়ে গেছে।
- T2 B-কে pop করে মুক্ত করে। top = C, আর
free(B)— B-এর মেমরি এখন allocator-এর, যেকোনো সময় অন্য কাজে যেতে পারে। - T2 আবার A push করে। A-কে (বা একই ঠিকানায় পাওয়া নতুন নোডকে) আবার ঢোকায়: A → C, top = A. মান আবার A — কিন্তু এটি “অন্য” A, যার next এখন C.
- T1 জাগে — CAS ভুল করে সফল। T1 করে CAS(top, A, B): top-এ A আছে, শর্ত মেলে, তাই top = B! কিন্তু B মুক্ত মেমরি। Stack এখন একটি মৃত নোডের দিকে ইঙ্গিত করছে — পরের pop মুক্ত মেমরি পড়বে, আর B-র জায়গায় অন্য ডেটা বসে গেলে পুরো stack এলোমেলো। কোনো error নেই, crash হবে পরে, অন্য কোথাও।
- সমাধান: pointer-এর সাথে version. top-কে (pointer, version) জোড়া বানাই, আর প্রতিটি সফল CAS version ১ বাড়ায়। T1 দেখেছিল (A, v0); এখন top (A, v3) — জোড়া মেলে না, CAS ব্যর্থ। T1 নতুন করে পড়ে: (A, v3), next = C; এবার CAS সফল, top = (C, v4). সঠিক ফল।
একই interleaving একটি ছোট simulator-এ — প্রথমে সাধারণ pointer CAS, তারপর version-সহ CAS:
// ABA on a lock-free stack: plain CAS vs CAS on (pointer, version)
function run(tagged) {
const next = { A: 'B', B: 'C', C: null }; // A -> B -> C
let top = { ptr: 'A', ver: 0 };
const freed = new Set();
const cas = (exp, ptr) => { // atomic compare-and-swap
const same = top.ptr === exp.ptr && (!tagged || top.ver === exp.ver);
if (same) top = { ptr, ver: top.ver + 1 };
return same;
};
const show = () => { let s = [], p = top.ptr;
while (p && s.length < 5) { s.push(p + (freed.has(p) ? '(freed!)' : '')); p = next[p]; }
return s.join(' -> ') + (tagged ? ` [ver ${top.ver}]` : ''); };
console.log(`--- ${tagged ? 'CAS on (pointer, version)' : 'plain pointer CAS'} ---`);
const seen = { ...top }, seenNext = next[seen.ptr]; // T1: old = A, next = B
console.log(`T1 reads top=${seen.ptr}, next=${seenNext}, then is preempted`);
cas({ ...top }, next[top.ptr]); console.log(`T2 pop A : ${show()}`);
cas({ ...top }, next[top.ptr]); freed.add('B'); console.log(`T2 pop B, free: ${show()}`);
next.A = top.ptr; cas({ ...top }, 'A'); console.log(`T2 push A : ${show()}`);
if (cas(seen, seenNext)) {
console.log(`T1 CAS(top: A -> B) succeeds -> ${show()} WRONG`);
} else {
console.log('T1 CAS fails: top is A again, but the version changed');
const old = { ...top }; // retry with fresh values
cas(old, next[old.ptr]); console.log(`T1 retries, pops A -> ${show()} correct`);
}
}
run(false);
run(true);আউটপুট:
--- plain pointer CAS ---
T1 reads top=A, next=B, then is preempted
T2 pop A : B -> C
T2 pop B, free: C
T2 push A : A -> C
T1 CAS(top: A -> B) succeeds -> B(freed!) -> C WRONG
--- CAS on (pointer, version) ---
T1 reads top=A, next=B, then is preempted
T2 pop A : B -> C [ver 1]
T2 pop B, free: C [ver 2]
T2 push A : A -> C [ver 3]
T1 CAS fails: top is A again, but the version changed
T1 retries, pops A -> C [ver 4] correct
প্রথম অংশে T1-এর CAS সফল, অথচ top এখন একটি মুক্ত নোড B-তে — WRONG. দ্বিতীয় অংশে একই ঘটনাক্রম, কিন্তু T2-এর তিনটি সফল CAS version-কে 0 থেকে 3-এ তুলেছে, তাই T1-এর (A, 0) মেলে না; সে নতুন মান পড়ে আবার চেষ্টা করে, আর stack-এ শুধু C থাকে (version 4). JavaScript এখানে শুধু ঘটনাক্রম দেখানোর জন্য; একটি আসল থ্রেড-ভিত্তিক বাগ ধরা কঠিন, কারণ T1-কে ঠিক ওই দুই instruction-এর মাঝে থামতে হয় — simulator-এ আমরা সেটি নিজের হাতে ঘটিয়েছি।
সমাধানের চার পথ — আর আসল সমস্যা “কখন মুক্ত করা নিরাপদ”
| কৌশল | মূল ধারণা | দাম বা সীমা |
|---|---|---|
| Tagged pointer (version counter) | pointer-এর সাথে একটি counter; প্রতি সফল CAS-এ বাড়ে। x86-64-এ ১৬ বাইটের cmpxchg16b দিয়ে pointer আর counter একসাথে, বা pointer-এর অব্যবহৃত বিটে counter | counter ঘুরে এলে তাত্ত্বিক ঝুঁকি (বড় counter-এ কার্যত শূন্য); মুক্ত মেমরি পড়ার সমস্যা পুরো মেটে না |
| Hazard pointer | প্রতিটি থ্রেড ঘোষণা করে “আমি এই নোডটি ছুঁয়ে আছি”; মুক্ত করার আগে সবার ঘোষণা দেখা হয়, কেউ ধরে থাকলে পরে। Maged Michael, ২০০৪ | প্রতি ব্যবহারে ঘোষণা আর memory fence-এর খরচ; কোড জটিল |
| Epoch-based reclamation | সময়কে “epoch”-এ ভাগ; মুছে ফেলা নোড অপেক্ষা করে যতক্ষণ না সব থ্রেড পরের epoch-এ যায়। Keir Fraser-এর ২০০৪ সালের কাজ থেকে জনপ্রিয়; Rust-এর crossbeam এটি ব্যবহার করে | একটি থ্রেড আটকে থাকলে মেমরি জমতেই থাকে |
| LL/SC | ARM, POWER, RISC-V-এর load-linked/store-conditional: মাঝে কেউ ঠিকানাটিতে লিখলেই store ব্যর্থ — মান একই হলেও | hardware-নির্ভর; C-র CAS যেভাবে compile হয়, তাতে সবসময় এই সুবিধা পাওয়া যায় না |
গভীরে তাকালে ABA আসলে একটি বড় প্রশ্নের অংশ: safe memory reclamation — lock-free কাঠামো থেকে সরানো নোড কখন free করা নিরাপদ, যখন অন্য থ্রেড হয়তো এখনো তার pointer হাতে নিয়ে আছে? Java বা Go-র মতো garbage collector-ওয়ালা ভাষায় সমস্যাটি অনেকটা মিটে যায়: কেউ reference রাখলে GC নোডটি মুক্ত করে না, তাই একই ঠিকানা ফিরে আসে না — Java-র ConcurrentLinkedQueue (Michael আর Scott-এর ১৯৯৬ সালের queue-র ভিত্তিতে) তাই তুলনামূলক সরল। C আর C++-এ কাজটি আপনার; পরের অংশের RCU এর একটি অসাধারণ উত্তর। C++26-এর জন্য hazard pointer আর RCU-র সাধারণ library সমর্থন গৃহীত হয়েছে বলে জানা যায়; আপনার compiler-এ এসেছে কি না, তার ডকুমেন্টেশনে দেখে নিন।
৬ · পড়া বেশি, লেখা কম — seqlock আর RCU
অনেক shared ডেটা প্রতি সেকেন্ডে লাখবার পড়া হয়, কিন্তু কদাচিৎ বদলায়: ঘড়ি, routing table, কনফিগারেশন। এখানে পর্ব ২৬-এর readers-writer lock-ও অপচয়, কারণ প্রতিটি পাঠককে একটি shared counter-এ লিখতে হয়, আর সেই cache line core থেকে core-এ ছোটে। দুটি কৌশল পাঠককে প্রায় বিনা খরচে পড়তে দেয়।
Seqlock — “পড়ার আগে আর পরে নম্বর মিলিয়ে দেখুন”
একটি sequence counter থাকে। লেখক লেখার আগে counter-কে বিজোড় করে, লেখা শেষে আবার জোড়। পাঠক কোনো lock নেয় না: পড়ার আগে counter দেখে (s1), ডেটা কপি করে, তারপর আবার counter দেখে (s2). s1 বিজোড় হলে, বা s1 ≠ s2 হলে, মাঝে লেখা হয়েছে — কপিটি ফেলে আবার পড়ে। বাস টার্মিনালের সময়সূচির বোর্ডের মতো: কর্মী বোর্ড বদলানোর সময় “হালনাগাদ হচ্ছে” বাতি জ্বালান; যাত্রী পড়ার আগে আর পরে বাতি আর বোর্ডের নম্বর দেখে নেন — মাঝে বদলে থাকলে আবার পড়েন।
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#define N 100000
static atomic_uint seq; /* even = stable, odd = write in progress */
static atomic_long x, y; /* the writer keeps y == -x */
#define LD(v, o) atomic_load_explicit(&(v), memory_order_##o)
#define ST(v, val, o) atomic_store_explicit(&(v), (val), memory_order_##o)
static void *writer(void *arg) {
(void)arg;
for (long i = 1; i <= N; i++) {
unsigned s = LD(seq, relaxed);
ST(seq, s + 1, relaxed); /* odd: "writing" */
atomic_thread_fence(memory_order_release);
ST(x, i, relaxed); ST(y, -i, relaxed); /* the pair */
ST(seq, s + 2, release); /* even: "done" */
}
return NULL;
}
static void *reader(void *arg) {
long *bad = arg;
for (int r = 0; r < N; r++) {
unsigned s1, s2; long a, b;
do {
s1 = LD(seq, acquire);
a = LD(x, relaxed); b = LD(y, relaxed);
atomic_thread_fence(memory_order_acquire);
s2 = LD(seq, relaxed);
} while ((s1 & 1) || s1 != s2); /* writer was busy: retry */
if (a != -b) (*bad)++; /* a torn pair would count */
}
return NULL;
}
int main(void) {
pthread_t w, r1, r2; long bad1 = 0, bad2 = 0;
pthread_create(&w, NULL, writer, NULL);
pthread_create(&r1, NULL, reader, &bad1);
pthread_create(&r2, NULL, reader, &bad2);
pthread_join(w, NULL); pthread_join(r1, NULL); pthread_join(r2, NULL);
printf("writer: %d updates, final pair (%ld, %ld), seq = %u\n",
N, LD(x, relaxed), LD(y, relaxed), LD(seq, relaxed));
printf("readers: %d reads, inconsistent reads: %ld\n", 2 * N, bad1 + bad2);
return 0;
}একটি আসল রানের আউটপুট:
writer: 100000 updates, final pair (100000, -100000), seq = 200000
readers: 200000 reads, inconsistent reads: 0
লেখক এক লাখবার জোড়াটি বদলেছে, প্রতিবার counter দুই করে বেড়েছে: ১,০০,০০০ × ২ = ২,০০,০০০ — শেষ seq হুবহু তা-ই, আর জোড়টি জানায় কোনো লেখা মাঝপথে নেই। দুই পাঠক মিলে দুই লাখবার পড়েছে, আর একবারও এমন জোড়া পায়নি যেখানে y ≠ −x — inconsistent reads: 0. কতবার আবার পড়তে হয়েছে, তা রান-ভেদে বদলায় বলে ছাপা হয়নি। দুটি সূক্ষ্মতা: (১) x আর y-ও atomic (relaxed) — নইলে পাঠকের “ফেলে দেওয়ার জন্য পড়া” C11-এ data race আর undefined behavior হতো; Hans Boehm ২০১২ সালে ঠিক এই প্রশ্নে একটি প্রবন্ধ লিখেছিলেন। (২) fence-গুলো নিশ্চিত করে যে ডেটার পড়া দুই counter-পড়ার মাঝখানেই থাকে (পর্ব ২৩-এর acquire/release).
Linux kernel-এ seqlock আর তার হালকা রূপ seqcount ব্যবহৃত হয় সময়ের হিসাবে — যেমন ৩২-বিট মেশিনে ৬৪-বিট jiffies_64 পড়া, আর timekeeping-এর তথ্য। সবচেয়ে সুন্দর উদাহরণ vDSO: clock_gettime system call ছাড়াই user space থেকে kernel-এর ভাগ করা একটি পাতা পড়ে, আর একটি sequence counter দিয়েই নিশ্চিত হয় যে মাঝপথে বদলানো সময় পড়া হয়নি। সীমা: পাঠক pointer অনুসরণ করতে পারে না (লেখক মাঝপথে নোড মুক্ত করলে পাঠক মুক্ত মেমরি পড়বে), লেখা ঘন ঘন হলে পাঠক বারবার ফেরত যায়, আর একাধিক লেখক থাকলে তাদের আলাদা একটি lock লাগে।
RCU — পাঠক কখনো অপেক্ষা করে না
Read-Copy-Update-এর নাম থেকেই কৌশল: হালনাগাদ করতে চাইলে পুরোনো ডেটা কপি করুন, কপিতে বদলান, তারপর একটি pointer এক ধাপে নতুন কপিতে সরিয়ে দিন। পাঠক lock ছাড়া pointer পড়ে — হয় পুরো পুরোনোটা দেখে, নয় পুরো নতুনটা, মাঝামাঝি কখনো না। কঠিন প্রশ্ন একটিই: পুরোনো কপি কখন মুছবেন? উত্তর: যখন নিশ্চিত হবেন যে pointer বদলানোর আগে শুরু হওয়া সব পাঠক শেষ করেছে — এই অপেক্ষার সময়টির নাম grace period. Linux-এর classic RCU-তে পাঠকের critical section-এ ঘুমানো বা preempt হওয়া চলে না; তাই প্রতিটি CPU একবার context switch করলে, idle হলে বা user mode-এ গেলে (quiescent state) বোঝা যায়, সেই CPU-র পুরোনো পাঠক শেষ। পাঠকের rcu_read_lock() তাই প্রায় বিনা খরচের — classic RCU-তে সেটি শুধু preemption বন্ধ করে।
| gcfg | |
| v1 {limit = 10} | |
| v2 {limit = 20} | |
| R1 (পাঠক) | |
| R2 (পাঠক) | |
| W (লেখক) | |
| grace period |
- শুরু। gcfg নির্দেশ করছে v1-কে (limit = 10). পাঠক R1
rcu_read_lock()করে pointer পড়েছে: তার হাতে v1. কোনো lock-এ লেখা হয়নি, কোনো atomic যোগ নেই। - Copy আর update. লেখক W নতুন মেমরি নেয়, v1-এর কপি বানায়, কপিতে limit = 20 বসায়। v2 এখনো কারও চোখে পড়ে না — gcfg এখনো v1-এ। লেখকদের নিজেদের মধ্যে সাধারণ একটি spinlock বা mutex থাকে; RCU শুধু পাঠকদের মুক্ত রাখে।
- Publish.
rcu_assign_pointer(gcfg, v2)— একটি pointer-লেখা, release ক্রমে, যাতে v2-এর ভেতরের মান pointer-এর আগে দৃশ্যমান হয়। এখন থেকে নতুন পাঠক v2 পায়। R1 এখনো v1 পড়ছে — আর সেটি ঠিক আছে: সে একটি সম্পূর্ণ, সামঞ্জস্যপূর্ণ পুরোনো ছবি দেখছে। - R2 আসে, W অপেক্ষা শুরু করে। R2
rcu_dereference(gcfg)করে পায় v2. W ডাকেsynchronize_rcu(): grace period শুরু। সে অপেক্ষা করবে শুধু publish-এর আগে শুরু হওয়া পাঠকদের জন্য — অর্থাৎ R1. R2 নতুন পাঠক, সে v1 কখনো দেখেনি, তাই তার জন্য অপেক্ষা নেই। - R1 শেষ। R1
rcu_read_unlock()করে। v1-এর দিকে আর কোনো pointer নেই — কিন্তু kernel এটি কীভাবে জানবে? প্রতিটি পাঠকের হিসাব রাখা ব্যয়বহুল, তাই kernel পরোক্ষ উপায় নেয়: প্রতিটি CPU একবার quiescent state পেরোলে সেই CPU-র পুরোনো সব পাঠক শেষ। - Grace period শেষ। সব CPU অন্তত একবার context switch করেছে বা idle ছিল — publish-এর আগে শুরু হওয়া কোনো পাঠক আর বেঁচে নেই।
synchronize_rcu()ফেরে। সাধারণত এতে কয়েক মিলিসেকেন্ড লাগে — লেখকের জন্য ধীর, কিন্তু পাঠকের জন্য খরচ শূন্য। - Free. W
kfree(v1)করে — নিরাপদ, কারণ কেউ v1 ধরে নেই। এখানে ABA বা মুক্ত মেমরি পড়ার কোনো সুযোগ নেই। অপেক্ষা না করে চলতে চাইলেcall_rcu()বাkfree_rcu(): grace period শেষে মোছার দায়িত্ব kernel-কে দিয়ে লেখক এগিয়ে যায়।
struct config { int limit; struct rcu_head rcu; };
static struct config __rcu *gcfg;
static DEFINE_SPINLOCK(cfg_lock); /* writers only */
int read_limit(void) /* reader: no lock, no wait */
{
int v;
rcu_read_lock();
v = rcu_dereference(gcfg)->limit;
rcu_read_unlock();
return v;
}
int set_limit(int limit) /* writer: copy, publish, wait, free */
{
struct config *new = kmalloc(sizeof(*new), GFP_KERNEL), *old;
if (!new)
return -ENOMEM;
spin_lock(&cfg_lock);
old = rcu_dereference_protected(gcfg, lockdep_is_held(&cfg_lock));
*new = *old; /* copy */
new->limit = limit; /* update the copy */
rcu_assign_pointer(gcfg, new); /* publish */
spin_unlock(&cfg_lock);
synchronize_rcu(); /* wait for old readers */
kfree(old); /* or: kfree_rcu(old, rcu); */
return 0;
}RCU-র জন্ম ১৯৯০-এর দশকে Sequent-এ, Paul McKenney আর Jack Slingwine-এর কাজে; ২০০২ সালে এটি Linux 2.5-এ আসে। আজ kernel-এ RCU-র API হাজার হাজার জায়গায় ব্যবহৃত — routing আর socket lookup, VFS-এর path lookup, প্রসেস আর module-এর তালিকা। User space-এর জন্য আছে liburcu. এর দাম আর ইতিহাস নিয়ে প্র ০৩ দেখুন।
আরও গভীরে: আধুনিক Linux-এ ডিফল্ট “Tree RCU”: হাজারো CPU-তে grace period-এর হিসাব একটি গাছের মতো কাঠামোয় ভাগ করা হয়, যাতে একটি global lock-এ সবাই হুমড়ি খেয়ে না পড়ে। Preemptible kernel-এ (PREEMPT_RT-সহ) পাঠক preempt হতে পারে, তাই “preemptible RCU” প্রতিটি task-এর পড়ার অবস্থা আলাদাভাবে গোনে; আর যেসব পাঠকের ঘুমানো দরকার, তাদের জন্য SRCU (sleepable RCU). সঠিকতা যাচাইয়ে আছে rcutorture নামের stress test.
৭ · Transactional memory আর lock ছাড়া অন্য মডেল
Transactional memory — ডেটাবেসের transaction মেমরিতে
ধারণাটি সুন্দর: প্রোগ্রামার শুধু লিখবেন “এই অংশটি atomic”, আর hardware বা runtime ডেটাবেসের মতো (DBMS কোর্সের transaction পর্ব) সেটিকে একটি transaction হিসেবে চালাবে — অন্য কারও সাথে সংঘাত না হলে সব লেখা একসাথে commit, হলে সব বাতিল (abort) করে আবার চেষ্টা। কোন lock, কোন ক্রমে — এসব ভাবনা নেই, তাই lock-ক্রমের deadlock-ও নেই। Maurice Herlihy আর Eliot Moss ১৯৯৩ সালে hardware transactional memory প্রস্তাব করেন; Nir Shavit আর Dan Touitou ১৯৯৫ সালে software রূপ।
বাস্তবতা মিশ্র। Intel ২০১৩ সালে Haswell প্রসেসরে TSX (Transactional Synchronization Extensions) আনে — দুটি অংশ: HLE (পুরোনো lock-কোডকে transaction হিসেবে চালানোর ইঙ্গিত) আর RTM (xbegin/xend). ২০১৪ সালে একটি hardware ত্রুটি (erratum) ধরা পড়ায় microcode হালনাগাদ দিয়ে Haswell আর প্রথম দিকের কিছু Broadwell-এ TSX বন্ধ করা হয়। ২০১৯ সালে TSX-সংশ্লিষ্ট একটি নিরাপত্তা দুর্বলতা (TAA) প্রকাশের পর আরও অনেক প্রসেসরে এটি বন্ধ করার বিকল্প আসে, আর পরে Intel-এর microcode হালনাগাদে বহু client প্রসেসরে TSX ডিফল্টভাবে বন্ধ হয়ে যায় — কোন মডেলে কী অবস্থা, তা Intel-এর ডকুমেন্টেশন আর আপনার OS-এর /proc/cpuinfo-তে rtm flag দেখে নিশ্চিত হতে হয়। IBM-এর কয়েকটি প্রসেসরেও hardware TM ছিল। সমস্যা মৌলিক: ডেটা cache-এ না ধরলে বা system call ও page fault-এ abort, তাই একটি সাধারণ lock-এর “fallback” পথ সবসময় লাগে (প্র ০২-এ বিস্তারিত)। Software TM টিকে আছে ভাষার স্তরে — Haskell-এর STM আর Clojure-এর ref.
Shared memory একেবারে বাদ — message passing
আরেকটি দর্শন: থ্রেডেরা মেমরি ভাগই করবে না, শুধু বার্তা পাঠাবে (পর্ব ১২-এর message passing-এর ধারণা, এবার প্রোগ্রামিং মডেল হিসেবে)। অফিসের ফাইল ক্যাবিনেটে সবাই হাত না দিয়ে, প্রতিটি ফাইলের একজন দায়িত্বপ্রাপ্ত কর্মী, আর অন্যরা তাঁকে চিরকুট পাঠান। Data race তখন গঠনগতভাবেই অসম্ভব — কিন্তু বাগ শেষ নয়।
-module(counter).
-export([start/0, loop/1]).
start() -> spawn(counter, loop, [0]). % a new actor with state 0
loop(N) ->
receive % one message at a time
{inc, From} -> From ! {ok, N + 1}, loop(N + 1);
{get, From} -> From ! {value, N}, loop(N)
end.Actor model (Carl Hewitt ও সহকর্মীরা, ১৯৭৩): প্রতিটি actor-এর নিজস্ব অবস্থা আর একটি mailbox; সে একবারে একটি বার্তা নিয়ে কাজ করে, তাই তার অবস্থায় race নেই। Ericsson-এ ১৯৮০-এর দশকের শেষে Joe Armstrong-দের তৈরি Erlang এই মডেলে লাখ লাখ হালকা প্রসেস চালায়, আর “let it crash” দর্শনে supervisor প্রসেস ব্যর্থ actor-কে আবার চালু করে — টেলিফোন সুইচের মতো সিস্টেমে উচ্চ নির্ভরযোগ্যতার জন্য বিখ্যাত। Akka (JVM) আর Orleans (.NET) একই ধারার। ফাঁদ: দুটি actor একে অপরের উত্তরের অপেক্ষায় থাকলে deadlock; mailbox অসীম বাড়তে পারে; আর “inc তারপর get” দুটি আলাদা বার্তা — মাঝে অন্যের বার্তা ঢুকলে check-then-act-এর atomicity violation ফিরে আসে, শুধু অন্য পোশাকে।
package main
import "fmt"
func main() {
jobs := make(chan int) // unbuffered: send waits for a receiver
results := make(chan int)
go func() { // a goroutine (lightweight thread)
for j := range jobs { results <- j * j }
close(results)
}()
go func() { for i := 1; i <= 3; i++ { jobs <- i }; close(jobs) }()
for r := range results { fmt.Println(r) } // 1 4 9
}Tony Hoare-এর ১৯৭৮ সালের CSP (Communicating Sequential Processes)-এ প্রক্রিয়ারা নামহীন, যোগাযোগ হয় channel দিয়ে। Go (২০০৯) এর প্রত্যক্ষ উত্তরসূরি; তার প্রবাদ: “Don't communicate by sharing memory; share memory by communicating.” কিন্তু Go-তে mutex-ও আছে, আর channel-এর নিজের বাগ আছে: কেউ পড়ছে না এমন channel-এ পাঠালে goroutine চিরকাল আটকে থাকে (goroutine leak), আর সব goroutine আটকে গেলে runtime জানায় fatal error: all goroutines are asleep - deadlock!. ২০১৯ সালের ASPLOS-এ Go-র বাস্তব বাগ নিয়ে একটি গবেষণা দেখায় যে blocking বাগের একটি বড় অংশ message passing থেকেই এসেছে — channel race-এর ওষুধ, deadlock-এর নয়। Go-র race detector (go test -race) ভেতরে ThreadSanitizer-এর runtime ব্যবহার করে।
let balance = 100;
async function withdraw(amount) {
if (balance >= amount) { // check
await logToServer(amount); // other tasks run here!
balance -= amount; // act -- on a stale check
}
}
// withdraw(80) and withdraw(80) started together -> balance = -60Node.js, Python-এর asyncio বা Rust-এর tokio-তে একটি event loop অনেক কাজকে অল্প থ্রেডে চালায়, আর কাজ বদলায় শুধু await-এর বিন্দুতে। এক থ্রেডে data race নেই, কিন্তু উপরের কোডে দুটি withdraw দুজনেই check পেরিয়ে await-এ থামে, তারপর দুজনেই টাকা কাটে — একই atomicity violation, await-এর ফাঁক দিয়ে। আরও দুই বিপদ: event loop-এ একটি ভারী হিসাব বা blocking কল পুরো সার্ভার থামিয়ে দেয় (সব কাজ সেই এক থ্রেডের অপেক্ষায়), আর await না করা promise বা task-এর ত্রুটি নীরবে হারিয়ে যায়। প্রতিকার: check আর act-এর মাঝে await না রাখা, বা async lock (asyncio.Lock). বিস্তারিত: JavaScript-এর event loop আর Python-এর asyncio.
৮ · বাগ ধরার হাতিয়ার — sanitizer থেকে model checking
পর্ব ১৫-এ (POSIX thread হাতে-কলমে) ThreadSanitizer আর Helgrind-এর আসল আউটপুট পড়েছেন — এখানে পুনরাবৃত্তি নয়, পুরো মানচিত্র। হাতিয়ারগুলোকে তিন দলে ভাবা যায়: চলমান প্রোগ্রাম পর্যবেক্ষণ, kernel-এর নিজের পাহারা, আর নকশা স্তরে সব সম্ভাব্য ক্রম যাচাই।
| টুল | কী ধরে | কোথায় | সীমা |
|---|---|---|---|
ThreadSanitizer (-fsanitize=thread) | data race (happens-before ধরে), কিছু lock-ক্রমের সমস্যা | GCC, Clang; Linux, macOS; Go-র -race | প্রোগ্রাম কয়েক গুণ ধীর; শুধু চলা কোড-পথ |
| Helgrind আর DRD (Valgrind) | race, lock-ক্রমের অসংগতি, pthread API-র ভুল ব্যবহার | Linux; recompile লাগে না | আরও ধীর; custom atomic কোডে ভুল সতর্কবার্তা আসতে পারে |
| lockdep | lock-এর ক্রমের চক্র — সম্ভাব্য deadlock, এমনকি যা এখনো ঘটেনি | Linux kernel (CONFIG_PROVE_LOCKING) | debug kernel-এ; ধীর |
| KCSAN | kernel-এর ভেতরের data race | Linux kernel (5.8 থেকে) | নমুনা নিয়ে কাজ করে, সব ধরে না |
| Model checking (TLA+, SPIN), CHESS/loom-এর মতো ক্রম-অনুসন্ধানী | নকশা বা ছোট কোডের সব সম্ভাব্য interleaving | নকশা স্তরে; Rust-এর loom | বড় অবস্থা-স্থানে বিস্ফোরণ; মডেল আর কোড আলাদা হতে পারে |
lockdep (Ingo Molnar, ২০০৬) বিশেষভাবে চমৎকার: প্রতিটি lock-কে একটি “class”-এ ফেলে, আর প্রতিবার কেউ A ধরে B নিলে A → B ধার যোগ করে। কখনো B → A ধার যোগ হলেই চক্র — পর্ব ২৭-এর resource-allocation graph-এর চক্র — আর kernel সঙ্গে সঙ্গে সতর্ক করে, যদিও দুটি পথ কখনো একসাথে চলেনি। বাস্তবের deadlock ঘটার অপেক্ষা না করে নিয়মভঙ্গ ধরা — ঠিক যেভাবে TSan race ধরে।
TLA+ Leslie Lamport-এর তৈরি একটি specification ভাষা; TLC model checker ছোট আকারের (যেমন ৩টি থ্রেড, ২টি আইটেম) সিস্টেমের প্রতিটি অবস্থা ঘুরে দেখে কোনো invariant ভাঙে কি না। Amazon Web Services-এর প্রকৌশলীরা ২০১৫ সালে Communications of the ACM-এ জানান, DynamoDB আর S3-এর মতো সিস্টেমের নকশায় TLA+ এমন সূক্ষ্ম বাগ ধরেছে, যা পরীক্ষা আর code review-এ ধরা পড়েনি। ABA বা Pathfinder-এর মতো বাগ ছোট মডেলেই বেরিয়ে আসে, কারণ model checker ঠিক “দুর্ভাগ্যজনক ক্রমগুলো”ই খোঁজে।
Concurrency-র সঠিকতার জন্য একটি স্তরে ভরসা যথেষ্ট নয়: নকশায় সরলতা (কম shared অবস্থা, স্পষ্ট মালিকানা), কোডে স্পষ্ট সমন্বয় (প্রতিটি ক্রম-অনুমান একটি sync), পরীক্ষায় sanitizer আর load, আর জটিল lock-free বা protocol-এর জন্য model checking. যত নতুন কৌশল (lock-free, RCU), তত বেশি যাচাই।
৯ · AI-এর যুগে concurrency-র কোড আর তথ্য
AI এক নিমেষে একটি “lock-free queue” লিখে দেবে, আর সেটি আপনার ল্যাপটপে হাজারবার ঠিক চলতেও পারে — এই পর্বের প্রতিটি বাগ সাধারণ পরীক্ষায় এভাবেই লুকিয়ে থাকে। সাধারণ ভুলগুলো:
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
Lock-free stack/queue-এ pop-এর পর সরাসরি free, কোনো reclamation নেই | ABA, মুক্ত মেমরি পড়া — চাপ বাড়লে crash | জিজ্ঞেস করুন “কখন free নিরাপদ?”; hazard pointer/epoch/RCU আছে কি না দেখুন; ASan আর TSan দিয়ে চাপের মধ্যে চালান |
সব জায়গায় memory_order_relaxed, বা volatile দিয়ে সমন্বয় | x86-এ চলে, ARM-এ (ফোন, Apple silicon, cloud-এর ARM সার্ভার) ভাঙে | প্রতিটি ordering-এর যুক্তি চান; ARM মেশিনেও পরীক্ষা করুন |
| “TSX/HTM দিয়ে দ্রুত করুন” — পুরোনো তথ্য | আপনার CPU-তে নির্দেশনাটি বন্ধ বা অনুপস্থিত | /proc/cpuinfo-তে flag দেখুন; সবসময় fallback পথ |
| Priority inversion-এর সমাধানে “H-এর priority আরও বাড়ান” | সমস্যা থেকেই যায় — অপেক্ষা L-এর জন্য, H-এর priority নয় | PTHREAD_PRIO_INHERIT/PROTECT, ছোট critical section |
RCU-র পাঠক অংশে ঘুমানো, বা rcu_read_lock-এর ভেতরে synchronize_rcu | kernel-এ সতর্কবার্তা বা চিরস্থায়ী অপেক্ষা | kernel-এর Documentation/RCU পড়ুন; lockdep আর CONFIG_PROVE_RCU চালু রেখে পরীক্ষা |
| Blocking-এর সীমা বা seqlock-এর counter-এর মতো হিসাবে ভুল সংখ্যা | schedulability-র ভুল সিদ্ধান্ত | অংশ ৩-এর মতো হাতে নিজে হিসাব করুন |
Concurrency-র কোড চাইলে পরিবেশ, progress guarantee আর যাচাই তিনটিই স্পষ্ট বলুন:
AI-এর লেখা concurrent কোড ব্যবহারের আগে — চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Priority inversion কী, আর Mars Pathfinder-এ কী হয়েছিল? — উচ্চ task নিম্ন task-এর lock-এর অপেক্ষায়, মাঝারি task নিম্নটিকে সরিয়ে রাখে; Pathfinder-এ watchdog বারবার রিসেট করত, priority inheritance চালু করে সারানো হয়েছিল বলে জানা যায়।
- Priority inheritance আর ceiling-এর পার্থক্য? — inheritance দরকারের সময় মালিককে অপেক্ষমাণের priority দেয়; ceiling আগে থেকে নির্ধারিত সীমা দিয়ে blocking এক critical section-এ সীমিত রাখে আর deadlock ঠেকায়।
- Lock-free আর wait-free-এর পার্থক্য? — lock-free: কেউ না কেউ সবসময় এগোয়; wait-free: প্রত্যেকেই সসীম ধাপে শেষ করে।
- ABA সমস্যা কী, সমাধান? — CAS মান একই দেখে সফল হয়, যদিও মাঝে মান বদলে ফিরে এসেছে; version tag, hazard pointer, epoch বা GC.
- RCU-তে পুরোনো ডেটা কখন মোছা হয়? — grace period-এর পরে, যখন publish-এর আগে শুরু হওয়া সব পাঠক শেষ (synchronize_rcu বা call_rcu).
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Mars Pathfinder-এর বাগ পৃথিবীতে পরীক্ষায় ধরা পড়েনি কেন, আর কোটি কোটি কিলোমিটার দূর থেকে কীভাবে সারানো গেল?
প্রথম কারণ concurrency বাগের চরিত্র নিজেই। Priority inversion ঘটতে তিনটি জিনিস একসাথে লাগে: নিম্ন task mutex ধরে আছে ঠিক তখন উচ্চ task-এর সেটি দরকার, আর ঠিক তখনই মাঝারি task-টি দীর্ঘক্ষণ চলার মতো কাজ পেয়েছে। প্রকল্পের প্রকৌশলীদের পরে দেওয়া বিবরণ অনুযায়ী, পরীক্ষার সময় এমন রিসেট কদাচিৎ দেখা গিয়েছিল, আর অবতরণের মতো বেশি জরুরি কাজের চাপে সেগুলো হার্ডওয়্যারের সাময়িক গোলমাল বলে ধরে নেওয়া হয়েছিল বলে জানা যায়। মঙ্গলে আবহাওয়া-তথ্যের কাজের চাপ বেশি হওয়ায় দুর্ভাগ্যজনক ক্রম ঘন ঘন ঘটতে থাকে। অংশ ১-এর গবেষণার কথাই মেলে: বাগটি ঘটাতে কয়েকটি নির্দিষ্ট মুহূর্তের ক্রম লাগে, যা সাধারণ পরীক্ষা এলোমেলোভাবে খুব কম ছোঁয়।
দ্বিতীয়ত, সারানো সম্ভব হয়েছিল কয়েকটি দূরদর্শী সিদ্ধান্তের জন্য। VxWorks-এর tracing সুবিধা চালু রেখে পৃথিবীতে একটি প্রতিরূপ যন্ত্রে দীর্ঘক্ষণ চালিয়ে ঘটনাটি পুনরায় ঘটানো হয়, আর trace দেখে কারণ বোঝা যায়। বিবরণগুলো অনুযায়ী যানটির সফটওয়্যারে একটি C interpreter-এর মতো সুবিধা রাখা ছিল, যা দিয়ে চলমান সিস্টেমে একটি বিশ্বব্যাপী সেটিং বদলে সংশ্লিষ্ট mutex-এ priority inheritance চালু করা গেছে — পুরো সফটওয়্যার নতুন করে পাঠাতে হয়নি। আর watchdog নিজেই ছিল একটি নিরাপত্তা-জাল: সিস্টেম পুরোপুরি অচল না হয়ে রিসেট হয়ে আবার কাজ শুরু করত, তাই যানটি বেঁচে ছিল।
এখান থেকে প্রকৌশলের কয়েকটি শিক্ষা আজও প্রাসঙ্গিক। এক, ডিফল্ট সেটিং গুরুত্বপূর্ণ — priority inheritance ছিল একটি বিকল্প, চালু ছিল না। দুই, “একবার দেখা গেছে, আর হয়নি” ধরনের ব্যর্থতা উপেক্ষা করা বিপজ্জনক; concurrency বাগ ঠিক এভাবেই নিজেকে দেখায়। তিন, মাঠে থাকা সিস্টেমে tracing আর নিরাপদ হালনাগাদের পথ রাখা — আজকের ভাষায় observability আর remote update — বাগ সারানোর সময় দিন থেকে ঘণ্টায় নামিয়ে আনে। চার, real-time সিস্টেমে blocking-এর সীমা (অংশ ৩) গণিতে প্রমাণ করা উচিত, ভাগ্যে নয়।
মূল উপলব্ধি: কোনো একটি task-এর কোড ভুল ছিল না; ভুল ছিল নীতিগুলোর মিলনে। Concurrency-র সঠিকতা পুরো সিস্টেমের সম্পত্তি, একটি ফাংশনের নয়।
প্র ০২Hardware transactional memory এত প্রতিশ্রুতি নিয়ে এসেও মূলধারায় এল না কেন?
Herlihy আর Moss ১৯৯৩ সালে যখন hardware TM প্রস্তাব করেন, যুক্তিটি জোরালো ছিল: lock-free কোড লেখা কঠিন, lock-এ deadlock আর inversion; অথচ CPU-র cache coherence ব্যবস্থা এমনিতেই জানে কোন core কোন cache line পড়ছে বা লিখছে — সেটি দিয়েই সংঘাত ধরা যায়। Multicore যুগে (২০০০-এর দশকের মাঝামাঝি) গবেষণা তুঙ্গে ওঠে; অনেকে ভেবেছিলেন এটিই parallel প্রোগ্রামিংয়ের ভবিষ্যৎ। Sun-এর Rock প্রসেসরে HTM ছিল, কিন্তু প্রসেসরটি বাজারে আসার আগেই বাতিল হয়। IBM-এর Blue Gene/Q আর POWER8, আর System z-এ HTM সত্যিই এসেছিল।
Intel-এর TSX (২০১৩, Haswell) ছিল সবচেয়ে বড় পরীক্ষা, আর তার ইতিহাস শিক্ষণীয়। ২০১৪ সালে একটি erratum ধরা পড়ায় microcode দিয়ে প্রথম দিকের প্রসেসরগুলোতে TSX বন্ধ করতে হয়। পরে ২০১৯ সালে TSX-এর abort-এর আচরণ কাজে লাগিয়ে তথ্য ফাঁস করার একটি side-channel দুর্বলতা (TAA) প্রকাশ পায়, আর তার পর অনেক প্রসেসরে এটি ডিফল্টভাবে বন্ধ হয়ে যায়; HLE অংশটি Intel পরে পরিত্যক্ত ঘোষণা করে। POWER-এর পরের প্রজন্মেও HTM-এর সমর্থন সরিয়ে নেওয়া হয়েছে বলে জানা যায়।
প্রযুক্তিগত বাধাও মৌলিক। Hardware transaction-এর আকার cache দিয়ে সীমিত — বড় ডেটা ছুঁলেই abort. System call, interrupt, page fault, এমনকি কিছু instruction-ও abort ঘটায়। তাই “best effort” HTM-এ সবসময় একটি সাধারণ lock-এর fallback পথ রাখতে হয়, আর fallback-এর সাথে transaction-কে মেলাতে গিয়ে জটিলতা ফিরে আসে। কার্যক্ষমতাও অনিশ্চিত: সংঘাত বেশি হলে বারবার abort-এ সময় নষ্ট, আর কেন abort হলো তা খুঁজে বের করা কঠিন। অর্থাৎ HTM সঠিকতার জটিলতা কমিয়েছিল, কিন্তু কার্যক্ষমতা আর পূর্বানুমানের জটিলতা বাড়িয়েছিল।
তবে ধারণাটি মরেনি। ডেটাবেস ইঞ্জিন আর কিছু lock elision-এর কাজে HTM কার্যকর প্রমাণিত হয়েছিল, Haskell-এর STM আর Clojure-এর ref software স্তরে সফল, আর ARM-ও নিজস্ব TM extension-এর specification প্রকাশ করেছে বলে জানা যায়।
মূল উপলব্ধি: একটি সমন্বয়-কৌশলের মূল্য শুধু “লেখা সহজ কি না” দিয়ে নয়, নির্ভরযোগ্য hardware, নিরাপত্তা, আর পূর্বানুমানযোগ্য কার্যক্ষমতা দিয়েও মাপা হয়।
প্র ০৩RCU একটি কোম্পানির পেটেন্ট-করা কৌশল থেকে কীভাবে Linux kernel-এর অন্যতম মৌলিক হাতিয়ার হলো, আর এর দাম কী?
১৯৯০-এর দশকে Sequent Computer Systems বড় multiprocessor সার্ভার বানাত, যেখানে কয়েক ডজন CPU একই kernel ডেটা পড়ত। সেখানে Paul McKenney আর Jack Slingwine লক্ষ করেন যে অনেক kernel কাঠামো — routing table, প্রসেসের তালিকা — প্রায় শুধু পড়া হয়; অথচ readers-writer lock-ও প্রতিটি পাঠককে একটি shared counter-এ লিখতে বাধ্য করে, আর সেই cache line CPU থেকে CPU-তে ছুটে বেড়ায়। তাঁদের সমাধান: পাঠক কিছুই লিখবে না; লেখক কপি করবে, pointer বদলাবে, আর “সব পুরোনো পাঠক শেষ হয়েছে” জানার জন্য scheduler-এর স্বাভাবিক ঘটনা (context switch) ব্যবহার করবে। ১৯৯৮ সালে এ নিয়ে প্রবন্ধ প্রকাশিত হয়; কৌশলটির পেটেন্ট ছিল Sequent-এর (পরে IBM-এর), আর IBM GPL-এর অধীনে Linux-এ ব্যবহারের অনুমতি দেয়। ২০০২ সালে এটি Linux 2.5-এ আসে।
প্রথমদিকে ব্যবহার ছিল অল্প, কিন্তু multicore যুগে সংখ্যাটি দ্রুত বেড়েছে — আজ kernel-এ RCU API-র ব্যবহার কয়েক হাজার থেকে দশ হাজারের ঘরে বলে McKenney-র বিভিন্ন লেখায় দেখা যায়। Networking-এর প্রতিটি packet-এর routing lookup, VFS-এর path lookup, আর অনেক তালিকা-চলাচল RCU-র ওপর দাঁড়িয়ে। কৌশলটি আরও বিকশিত হয়েছে: Tree RCU হাজারো CPU-তে, preemptible RCU real-time kernel-এ, SRCU ঘুমাতে পারা পাঠকের জন্য, আর liburcu user space-এ।
দাম তিনটি, আর তা ভালো করে বুঝতে হয়। এক, লেখক ধীর — synchronize_rcu() মিলিসেকেন্ড স্তরে অপেক্ষা করতে পারে; লেখা ঘন ঘন হলে RCU ভুল হাতিয়ার। দুই, মেমরি: grace period শেষ না হওয়া পর্যন্ত পুরোনো কপি থাকে; অনেক দ্রুত হালনাগাদে অপেক্ষমাণ কপি জমে মেমরির চাপ বাড়াতে পারে। তিন, মানসিক মডেল: পাঠক হয়তো একটু পুরোনো সংস্করণ দেখছে — এটি গ্রহণযোগ্য কি না, তা নকশার সিদ্ধান্ত। Routing table-এ কয়েক মিলিসেকেন্ড পুরোনো তথ্য সাধারণত চলে; ব্যাংকের ব্যালান্সে চলে না।
মূল উপলব্ধি: RCU শেখায় যে সমন্বয়ের খরচ সমান ভাগ করার দরকার নেই — বিরল কাজের ওপর (লেখা) পুরো খরচ চাপিয়ে সাধারণ কাজকে (পড়া) প্রায় বিনামূল্যে করা যায়, যদি “একটু পুরোনো ছবি” মেনে নেওয়া যায়।
প্র ০৪Lock-free মানেই কি দ্রুত? কখন একটি সাধারণ mutex বেশি ভালো?
“Lock-free” একটি progress guarantee, গতির প্রতিশ্রুতি নয়। এর আসল সুবিধা হলো কেউ থেমে গেলেও অন্যরা আটকায় না — তাই priority inversion, convoy আর lock-ধারী থ্রেডের preemption-এর সমস্যা থাকে না। কিন্তু গতির হিসাব আলাদা। প্রতিটি CAS একটি atomic read-modify-write, যা একটি cache line-এর একচেটিয়া মালিকানা চায়; অনেক core একই top-এ CAS করলে সেই cache line core থেকে core-এ ছুটে বেড়ায়, আর ব্যর্থ CAS-এর পুনরাবৃত্তি কাজ নষ্ট করে। উচ্চ সংঘাতে একটি Treiber stack একটি ভালো mutex-এর চেয়ে ধীর হতে পারে, কারণ mutex অন্তত অপেক্ষমাণদের ঘুম পাড়িয়ে cache-এর টানাটানি থামায়। এ কারণে গবেষকরা elimination backoff (একটি push আর একটি pop stack না ছুঁয়েই পরস্পরকে মিটিয়ে নেয়) বা flat combining (একজন থ্রেড সবার অনুরোধ একসাথে করে)-এর মতো কৌশল এনেছেন।
দ্বিতীয় দাম জটিলতা। অংশ ৫-এ দেখেছেন, সঠিক lock-free কোডের অর্ধেক কাজ memory reclamation — hazard pointer বা epoch ছাড়া C-তে প্রায় অসম্ভব। Memory ordering-এর ভুল x86-এ লুকিয়ে থাকে, ARM-এ বেরিয়ে আসে। আর lock-free কাঠামো একটির বেশি অবস্থান একসাথে বদলাতে পারে না: “এক তালিকা থেকে সরিয়ে আরেকটিতে বসানো” atomic-ভাবে করা যায় না, mutex দিয়ে খুব সহজে যায়।
বাস্তব পরামর্শ তাই রক্ষণশীল। প্রথমে সাধারণ mutex; মাপুন (perf, lock contention-এর পরিসংখ্যান)। Contention দেখা দিলে প্রথমে নকশা বদলান: lock ভাগ করা, প্রতি-CPU বা প্রতি-থ্রেড ডেটা (পর্ব ১৫-এর “নিজের ঘরে লিখুন”), পড়া-প্রধান ডেটায় RCU বা seqlock. তারপরও দরকার হলে প্রমাণিত library-র lock-free কাঠামো — Java-র java.util.concurrent, C++-এর Folly বা Boost.Lockfree, Rust-এর crossbeam — নিজে লেখা নয়। নিজে লিখতে হলে model checker আর stress test বাধ্যতামূলক। আধুনিক Linux-এর futex-ভিত্তিক mutex অসংঘাতের ক্ষেত্রে শুধু একটি atomic কাজ, kernel-এ যায়ই না — তাই “mutex ধীর” ধারণাটিও প্রায়ই পুরোনো।
মূল উপলব্ধি: Lock-free বেছে নেওয়ার কারণ হওয়া উচিত progress-এর নিশ্চয়তা বা মাপা contention, ফ্যাশন নয়। মাপুন, তারপর নকশা বদলান, শেষে lock-free.
প্র ০৫Message passing data race দূর করে — তাহলে কি concurrency বাগও দূর হয়? Erlang, Go আর async/await-এর অভিজ্ঞতা কী বলে?
Message passing একটি বড় শ্রেণির বাগ সত্যিই দূর করে: দুটি থ্রেড একই মেমরিতে সমন্বয় ছাড়া লিখতে পারে না, কারণ তারা মেমরিই ভাগ করে না। Erlang-এর প্রতিটি প্রসেসের নিজস্ব heap, বার্তা কপি হয়ে যায়; এটি টেলিফোন সুইচের মতো সিস্টেমে বহু বছর ধরে নির্ভরযোগ্যতা দিয়েছে বলে সুপরিচিত, আর WhatsApp-এর মতো সেবার backend-এ Erlang-এর ব্যবহার প্রায়ই উদ্ধৃত হয়। কিন্তু অংশ ১-এর তিনটি শ্রেণির মধ্যে message passing মূলত একটির দাওয়াই।
Atomicity violation থেকে যায়: একটি actor-কে “ব্যালান্স কত?” জিজ্ঞেস করে উত্তর পেয়ে টাকা তোলার বার্তা পাঠালে মাঝের সময়ে অন্য কেউ টাকা তুলে নিতে পারে — check-then-act, শুধু বার্তার ভাষায়। সমাধানও একই: পুরো কাজটি একটি বার্তায় (“পর্যাপ্ত থাকলে তবেই তোলা হবে” ধরনের অনুরোধ)। Order violation থেকে যায়: বার্তা পৌঁছানোর ক্রম সব জোড়ার মধ্যে নিশ্চিত নয়। Deadlock তো স্পষ্টভাবেই থাকে: A B-এর উত্তরের অপেক্ষায়, B A-এর। Go-র বাস্তব বাগ নিয়ে ২০১৯ সালের ASPLOS-এর গবেষণা (Tengfei Tu ও সহকর্মীরা) দেখায়, Docker, Kubernetes, etcd-এর মতো প্রকল্পে blocking বাগের বড় একটি অংশ channel-এর ভুল ব্যবহার থেকে এসেছে — যেমন কেউ পড়বে না এমন channel-এ পাঠানো goroutine চিরকাল আটকে থাকা। অর্থাৎ message passing একটি সমস্যা কমিয়ে আরেকটি নতুন রূপে এনেছে।
Async/await-ও একই গল্প। একটি থ্রেডে data race নেই, তাই অনেকে ভাবেন race-ও নেই। কিন্তু প্রতিটি await একটি “এখানে অন্যরা চলতে পারে” বিন্দু, আর check ও act-এর মাঝে await পড়লে অংশ ৭-এর withdraw বাগ। Rust ভিন্ন পথ নিয়েছে: type system (Send, Sync, ownership) compile-এর সময়ই data race অসম্ভব করে, কিন্তু deadlock বা logic-এর race নয়।
মূল উপলব্ধি: প্রতিটি concurrency মডেল কিছু বাগ গঠনগতভাবে অসম্ভব করে, বাকিগুলো থেকে যায়। প্রশ্ন সবসময় একই: কোন কাজগুলো অবিভাজ্য হতে হবে, কোন ক্রম নিশ্চিত করতে হবে, আর কে কার অপেক্ষায় থাকতে পারে।
অনুশীলন
হাতে হিসাব, ট্রেস, আউটপুট অনুমান আর কোড লেখা — আগে নিজে চেষ্টা করুন, তারপর উত্তর মেলান।
-
হাতে হিসাব (blocking): চারটি task, অগ্রাধিকার τ1 > τ2 > τ3 > τ4, এক প্রসেসর। τ1 lock S1 আর S2 নেয়; τ2 নেয় শুধু S2; τ3 S1 ধরে রাখে সর্বোচ্চ ৫ ms; τ4 S2 ধরে রাখে সর্বোচ্চ ২ ms. (ক) প্রতিটি lock-এর ceiling কত? (খ) τ1 আর τ2-এর worst-case blocking PIP-এ কত, PCP-তে কত?
(ক) S1 নেয় τ1 আর τ3 → ceiling(S1) = τ1. S2 নেয় τ1, τ2, τ4 → ceiling(S2) = τ1.
(খ) τ1: PIP-এ τ3 আর τ4 দুজনেই একবার করে আটকাতে পারে: ৫ + ২ = ৭ ms. PCP-তে একটিই: max(৫, ২) = ৫ ms.
τ2: নিম্ন task τ3 আর τ4. τ4 τ2-কে সরাসরি আটকাতে পারে (S2), আর τ3 আটকাতে পারে “push-through” পথে — যখন τ3 S1-এর কারণে τ1-এর অগ্রাধিকার পায়, তখন τ2 চলতে পারে না। PIP-এ দুজনেই একবার করে: ৫ + ২ = ৭ ms. PCP-তে দুটি lock-এরই ceiling τ1 ≥ τ2, তাই বড়জোর একটি critical section: max(৫, ২) = ৫ ms.
-
Seqlock ট্রেস: counter শুরুতে 0. লেখক তিনবার লেখে; প্রতিবার counter +১ (শুরু) আর +১ (শেষ)। একটি পাঠক চারবার চেষ্টা করে, প্রতিবার (s1, s2) পায়: (ক) (2, 2) (খ) (3, 4) (গ) (4, 6) (ঘ) (6, 6). কোন চেষ্টা গ্রহণযোগ্য, কোনটি আবার পড়তে হবে, আর প্রতিটি গ্রহণযোগ্য পড়ায় কয়টি লেখা সম্পূর্ণ হয়েছিল?
(ক) গ্রহণযোগ্য — জোড়, আর দুটি সমান; ২ ÷ ২ = ১টি লেখা সম্পূর্ণ। (খ) বাতিল — s1 বিজোড় মানে পড়া শুরুর সময় দ্বিতীয় লেখা চলছিল। (গ) বাতিল — দুটিই জোড়, কিন্তু অসমান: পড়ার মাঝে তৃতীয় একটি পুরো লেখা ঘটে গেছে, তাই x আর y দুই ভিন্ন লেখার হতে পারে। (ঘ) গ্রহণযোগ্য — ৬ ÷ ২ = ৩টি লেখাই সম্পূর্ণ। (গ) দেখায় কেন শুধু “জোড় কি না” দেখা যথেষ্ট নয়, s1 = s2-ও লাগে।
-
আউটপুট অনুমান: অংশ ৪-এর Treiber stack প্রোগ্রামটি বদলে দুইয়ের জায়গায় চারটি থ্রেড চালানো হলো, প্রতিটি ৫০০টি করে push করে, base 0, 500, 1000, 1500. শেষ লাইনে কী ছাপা হবে? এবার ধরুন pop-এর লুপটি join-এর আগে আরেকটি থ্রেডে একসাথে চলছে, আর
freeকরছে — তখন কি একই নিশ্চয়তা দেওয়া যায়?প্রথম অংশে ঠিক আগের মতোই:
popped 2000 nodes, sum 1999000 (expected 1999000), duplicates 0— চার থ্রেডের মান মিলে ০ থেকে ১৯৯৯ সবকটি একবার করে, আর গোনা ও যোগফল ক্রমের ওপর নির্ভর করে না। দ্বিতীয় অংশে না: push আর pop একসাথে চললে এবং pop করা নোড মুক্ত হয়েmalloc-এ আবার ফিরে এলে অংশ ৫-এর ABA ঘটতে পারে, আর pop-এold->nextপড়া মুক্ত মেমরি পড়াও হতে পারে। সেজন্য hazard pointer, epoch বা version tag লাগবে। -
কোড লিখুন: একটি C ফাংশন
int make_pi_mutex(pthread_mutex_t *m)লিখুন, যা priority inheritance-সহ একটি mutex তৈরি করে, প্রতিটি ধাপের ফেরত মান যাচাই করে, আর ব্যর্থ হলে সেই error নম্বর ফেরত দেয়।#include <pthread.h> int make_pi_mutex(pthread_mutex_t *m) { pthread_mutexattr_t a; int rc = pthread_mutexattr_init(&a); if (rc != 0) return rc; rc = pthread_mutexattr_setprotocol(&a, PTHREAD_PRIO_INHERIT); if (rc == 0) rc = pthread_mutex_init(m, &a); pthread_mutexattr_destroy(&a); /* attr is no longer needed */ return rc; /* 0 = success, else ENOTSUP, EINVAL ... */ }মনে রাখুন: pthread ফাংশন error দেয় ফেরত মানে, errno-তে নয় (পর্ব ১৫)।
ENOTSUPআসতে পারে যদি প্ল্যাটফর্মে protocol-টি সমর্থিত না হয়। Inheritance কাজে আসবে তখনই, যখন থ্রেডগুলো real-time নীতিতে (SCHED_FIFO/SCHED_RR) আলাদা অগ্রাধিকারে চলে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমRCU দিয়ে একটি shared কাঠামো হালনাগাদের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- লেখক লেখকদের lock নেয় আর পুরোনো সংস্করণের একটি কপি বানায়
- কপিতে নতুন মান বসানো হয় — এখনো কোনো পাঠক তা দেখে না
rcu_assign_pointerদিয়ে shared pointer নতুন কপিতে সরানো হয়synchronize_rcu()grace period শেষ হওয়া পর্যন্ত অপেক্ষা করে- পুরোনো সংস্করণটি
kfreeদিয়ে মুক্ত করা হয়
একটি real-time সিস্টেমে তিনটি task: H (উচ্চ), M (মাঝারি), L (নিম্ন). L একটি mutex ধরে আছে, H সেটির জন্য blocked, আর M (যার mutex লাগে না) চলছে। H-এর অপেক্ষা সীমিত করার সবচেয়ে উপযুক্ত উপায় কোনটি?
- H-এর অগ্রাধিকার আরও বাড়ানো
- Mutex-এ priority inheritance (যেমন
PTHREAD_PRIO_INHERIT) চালু করা - M-কে স্থায়ীভাবে L-এর নিচে নামিয়ে দেওয়া
- Watchdog-এর সময়সীমা কমিয়ে দ্রুত রিসেট করা
কোন progress guarantee নিশ্চিত করে যে প্রতিটি থ্রেড, অন্য থ্রেডরা যত দ্রুত বা ধীরেই চলুক, নিজের কাজ সসীম সংখ্যক ধাপে শেষ করবে?
- Obstruction-free
- Lock-free
- Wait-free
- Blocking (mutex-ভিত্তিক)
একটি lock-free stack-এ থ্রেড T1 top = A আর A-এর next = B পড়ে থেমে যায়। এর মধ্যে অন্য থ্রেড A আর B pop করে, B মুক্ত করে, আর A আবার push করে। T1 এবার CAS(top, A, B) করে। কী ঘটে?
- CAS সফল হয়, আর top একটি মুক্ত নোডে চলে যায় — ABA সমস্যা
- CAS ব্যর্থ হয়, কারণ মাঝে stack বদলেছে
- দুই থ্রেড deadlock-এ পড়ে
- CPU একটি exception দিয়ে প্রোগ্রাম থামায়
একটি seqlock-এ পাঠক পড়ার আগে sequence counter-এ 10 আর পড়ার পরে 12 দেখল। পাঠকের কী করা উচিত?
- ডেটা গ্রহণ করা, কারণ দুটি মানই জোড়
- লেখকের lock নিয়ে লেখককে থামানো
- দুটি সংস্করণের গড় নেওয়া
- কপিটি ফেলে দিয়ে আবার পড়া
RCU-তে লেখক shared pointer নতুন সংস্করণে সরানোর পর পুরোনো সংস্করণ কখন মুক্ত করা নিরাপদ?
- Pointer সরানোর সঙ্গে সঙ্গে
- Grace period শেষ হলে — যখন সরানোর আগে শুরু হওয়া সব পাঠক শেষ করেছে
- যখন সিস্টেমে আর কোনো পাঠকই নেই
- যখন পুরোনো সংস্করণের reference count শূন্য হয়
একটি JavaScript async ফাংশনে if (balance >= amount) { await log(); balance -= amount; } লেখা আছে, আর দুটি কল একসাথে শুরু হয়। Node.js এক থ্রেডে চললেও balance ঋণাত্মক হতে পারে কেন?
- দুটি CPU core একই সাথে balance-এ লেখে
- JavaScript-এর number-এর precision সমস্যা
- check আর বিয়োগের মাঝে await থাকায় অন্য কলটি মাঝে চলে, ফলে একটি atomicity violation হয়
- Garbage collector balance-এর মান বদলে দেয়
বাস্তব ওপেন-সোর্স সফটওয়্যারের concurrency বাগ নিয়ে Lu ও সহকর্মীদের (ASPLOS 2008) গবেষণা অনুযায়ী non-deadlock বাগের প্রায় সবই কোন দুই শ্রেণিতে পড়ে?
- Atomicity violation আর order violation
- Priority inversion আর starvation
- Livelock আর thundering herd
- ABA সমস্যা আর memory leak
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- তিন শ্রেণির বাগ: atomicity violation (যাচাই আর ব্যবহারের মাঝে ফাঁক), order violation (ক্রমের অনুমান কোডে নেই), deadlock. প্রতিটি ক্রম-অনুমান একটি সমন্বয় হয়ে কোডে থাকবে।
- Priority inversion সীমাহীন হয় মাঝারি task-এর কারণে; inheritance blocking-কে L-এর critical section-এ সীমিত করে, ceiling protocol এক critical section-এ আর deadlock ঠেকায়।
- Lock-free = কেউ না কেউ এগোয়, wait-free = প্রত্যেকে এগোয়। Treiber stack-এ একটি pointer আর CAS; ব্যর্থ CAS মানে অন্য কারও সফলতা।
- ABA আসে মুক্ত মেমরি ফিরে এলে; সমাধান version tag, hazard pointer, epoch, বা GC. আসল প্রশ্ন: কখন free নিরাপদ।
- Seqlock: পড়া, নম্বর মেলানো, না মিললে আবার পড়া; RCU: copy, publish, grace period, তারপর free. মডেল বদলালেও (actor, channel, async) atomicity, ক্রম আর অপেক্ষার চক্রের প্রশ্ন থেকে যায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩০ · মেমরির ভিত্তি — address binding, logical আর physical addressপরবর্তী পর্বমডিউল ৫-এর শুরু: প্রোগ্রামের ঠিকানা কীভাবে আসল মেমরিতে পৌঁছায়।
- অপারেটিং সিস্টেম · কোর্স হোম৬৪টি পর্বপুরো কোর্সের রোডম্যাপ — মডিউল ৪-এর সব পর্ব একসাথে।
- পর্ব ১৫ · POSIX thread হাতে-কলমেআগের পর্বThreadSanitizer আর Helgrind-এর আসল আউটপুট, data race আর thread pool.
- DBMS · Concurrency controlএকই সমস্যা, অন্য জগৎLock, MVCC আর transaction — RCU-র “পুরোনো সংস্করণ রাখা” ধারণার ডেটাবেস-রূপ।
- Python · asyncioপাশের ট্র্যাকEvent loop আর async/await — await-এর ফাঁকের race-সহ।