এই পর্বে যা শিখবেন
- Semaphore থাকার পরও কেন আলাদা হাতিয়ার লাগে — শর্তের জন্য busy-waiting আর ছড়িয়ে থাকা P/V-এর ভুল
- Condition variable-এর তিনটি অপারেশন, wait-এর ভেতরে mutex ছাড়া আর ফিরে পাওয়ার ধাপগুলো, আর একটি এক-ঘরের mailbox
ifদিয়ে wait কেন ভুল আর lost wakeup কীভাবে ঘটে — দুটি interleaving ধাপে ধাপে- Monitor, Hoare বনাম Mesa semantics-এর সুনির্দিষ্ট পার্থক্য আর তার ফল; semaphore দিয়ে monitor বানানোর পাঠ্যবইয়ের পদ্ধতি
- Java, C#, Go, Python আর Windows-এ একই ধারণা; resource allocator monitor; barrier আর latch
১ · Semaphore-এর পরে কী বাকি থাকে — শর্তের জন্য অপেক্ষা
পর্ব ২২-এ দেখেছেন critical section কী, আর পর্ব ২৪-এ mutex আর semaphore. Mutex একটি প্রশ্নের উত্তর দেয়: “এই মুহূর্তে কি অন্য কেউ ভেতরে আছে?” কিন্তু concurrent প্রোগ্রামে আরেকটি প্রশ্নও বারবার আসে: “আমি যে অবস্থার জন্য অপেক্ষা করছি, সেটি কি এসেছে?” যেমন — queue-তে কি কোনো কাজ আছে? buffer-এ কি জায়গা খালি? সব worker কি প্রস্তুত? এই অবস্থাগুলোকে বলে শর্ত বা predicate.
একটি সরকারি অফিসের ফাইল-কক্ষ ভাবুন। কক্ষে একবারে একজনই ঢুকতে পারেন — দরজায় একটি চাবি (mutex). আপনি ঢুকে দেখলেন, যে ফাইলটি চাই সেটি এখনো আসেনি। এখন কী করবেন? চাবি হাতে নিয়ে কক্ষেই দাঁড়িয়ে থাকলে ফাইল নিয়ে আসা কর্মী ঢুকতেই পারবেন না। আর বারবার বেরিয়ে-ঢুকে দেখলে সারাদিন নষ্ট। ভালো ব্যবস্থা হলো: চাবি দারোয়ানের হাতে ফেরত দিয়ে বারান্দার বেঞ্চে বসা, আর দারোয়ানকে বলে রাখা “ফাইল এলে আমাকে ডাকবেন”। ডাক পেলে আবার চাবি নিয়ে ঢুকে দেখা — ফাইলটি সত্যিই আছে তো? এই বেঞ্চটিই condition variable.
প্রথম ভুল পথ — busy-waiting
সবচেয়ে সরল সমাধান একটি লুপে শর্তটি বারবার দেখা:
pthread_mutex_lock(&m);
while (count == 0) { /* nothing to take yet */
pthread_mutex_unlock(&m); /* let the producer in ... */
pthread_mutex_lock(&m); /* ... and look again at once */
}
item = take(); count--;
pthread_mutex_unlock(&m);এটি কাজ করে, কিন্তু খুব খরুচে। একে বলে busy-waiting: অপেক্ষারত থ্রেড ঘুমায় না, CPU-তে বসে লক্ষ লক্ষবার একই প্রশ্ন করে। এক core-এর মেশিনে আরও খারাপ — যে থ্রেড শর্তটি সত্য করবে, সে CPU-ই পায় না যতক্ষণ না অপেক্ষাকারীর time slice শেষ হয়। পর্ব ২৩-এর spinlock-এ খুব অল্প সময়ের জন্য ঘোরা যুক্তিসংগত; কিন্তু “queue-তে কাজ আসা” কয়েক মিলিসেকেন্ড থেকে কয়েক ঘণ্টা পরেও হতে পারে। তখন ঘুমানোই একমাত্র ঠিক পথ।
দ্বিতীয় পথ — semaphore দিয়ে শর্ত
Semaphore দিয়ে ঘুমানো যায়, আর Dijkstra-র P/V দিয়ে প্রায় সব সমন্বয়ই লেখা সম্ভব। পর্ব ২৬-এর bounded buffer-এ দেখবেন তিনটি semaphore: mutex, empty (খালি ঘরের সংখ্যা) আর full (ভরা ঘরের সংখ্যা)। তবে বড় প্রোগ্রামে semaphore-এর দুর্বলতা স্পষ্ট হয়:
- একই হাতিয়ার দুই কাজে: একটি semaphore কখনো তালা (mutual exclusion), কখনো গণনা (কতগুলো ঘর খালি), কখনো সংকেত। কোডে
P(s)দেখে বোঝার উপায় নেই কোনটি কোন উদ্দেশ্যে। - ক্রম ভুল হলে deadlock: bounded buffer-এর producer-এ
P(empty); P(mutex);-এর জায়গায়P(mutex); P(empty);লিখলে buffer ভরা থাকলে producer mutex হাতে নিয়েই ঘুমায় — consumer আর ঢুকতে পারে না, দুজনেই চিরকাল আটকে (পর্ব ২৭-এর deadlock). - ছড়িয়ে থাকা P আর V: একটি
Vভুলে গেলে কেউ চিরকাল ঘুমায়, একটি বাড়তিVদিলে দুজন একসাথে critical section-এ। এই P আর V প্রোগ্রামের নানা ফাইলে ছড়িয়ে থাকে, তাই কোনো একটি জায়গা দেখে সঠিকতা যাচাই করা যায় না। - জটিল শর্ত প্রকাশ করা কঠিন: “অন্তত ৩টি ঘর খালি এবং কোনো writer নেই” — এমন শর্ত একটি সংখ্যা দিয়ে প্রকাশ করা যায় না।
১৯৭০-এর দশকের শুরুতে ঠিক এই সমস্যাগুলো দেখে Per Brinch Hansen আর C. A. R. (Tony) Hoare একটি কাঠামোবদ্ধ বিকল্প প্রস্তাব করেন — monitor, যার ভেতরের অপেক্ষার হাতিয়ার condition variable. অংশ ৫-এ monitor-এর পুরো গল্প; তার আগে condition variable-কে আলাদা করে বুঝে নিই, কারণ আজকের pthreads-এ সেটিই সরাসরি পাওয়া যায়।
২ · Condition variable — wait, signal আর broadcast
Condition variable নিজে কোনো মান রাখে না — এটি শুধু ঘুমন্ত থ্রেডের একটি সারি। এর সাথে সবসময় থাকে একটি mutex (যা shared ডেটা রক্ষা করে) আর একটি শর্ত (যা ওই shared ডেটা নিয়ে একটি প্রশ্ন)। অপারেশন তিনটি:
| অপারেশন (pthreads) | কী করে | ফাইল-কক্ষের ভাষায় |
|---|---|---|
pthread_cond_wait(&cv, &m) | ডাকার সময় m ধরা থাকতে হবে। একটি অবিভাজ্য ধাপে থ্রেডকে cv-এর সারিতে রাখে আর m ছাড়ে; জাগলে m আবার নেয়, তারপর ফেরে | চাবি ফেরত দিয়ে বেঞ্চে বসা; ডাক পেলে চাবি নিয়ে আবার ঢোকা |
pthread_cond_signal(&cv) | সারিতে কেউ থাকলে অন্তত একজনকে জাগায়; কেউ না থাকলে কিছুই করে না | দারোয়ান বেঞ্চের একজনকে ডাকেন |
pthread_cond_broadcast(&cv) | সারির সবাইকে জাগায় | “সবাই আসুন” — বেঞ্চের সবাই উঠে পালা করে চাবি নেন |
দুটি কথা শুরুতেই মনে রাখুন। প্রথমত, condition variable-এর কোনো স্মৃতি নেই. কেউ অপেক্ষা না করার সময় signal দিলে তা হাওয়ায় মিলিয়ে যায়। Semaphore-এর V কিন্তু মনে রাখে — মান ১ বাড়ে, পরের P আর ঘুমায় না। এই পার্থক্যই অংশ ৪-এর lost wakeup-এর মূল। দ্বিতীয়ত, “অবিভাজ্যভাবে mutex ছাড়া আর ঘুমানো” অংশটিই condition variable-এর আসল জাদু: ছাড়া আর ঘুমের মাঝখানে কোনো ফাঁক থাকে না, যেখানে signal হারাতে পারে।
সঠিক ব্যবহারের ছাঁচ সবসময় একই, প্রায় মুখস্থ করার মতো:
/* waiting side */ /* changing side */
pthread_mutex_lock(&m); pthread_mutex_lock(&m);
while (!condition) change_shared_state(); /* makes it true */
pthread_cond_wait(&cv, &m); pthread_cond_signal(&cv); /* or broadcast */
use_shared_state(); /* condition holds */ pthread_mutex_unlock(&m);
pthread_mutex_unlock(&m);নিচের ডায়াগ্রামে একটি consumer (C) আর একটি producer (P) এই ছাঁচ মেনে এক-ঘরের একটি mailbox ব্যবহার করছে। দেখুন wait-এর ভেতরে mutex কখন ছাড়া হয়, কখন ফেরত আসে:
1C: lock(&m);2C: while (!full)3C: cond_wait(¬_empty, &m);4C: x = slot; full = 0; unlock(&m);5P: lock(&m); slot = 10; full = 1;6P: cond_signal(¬_empty);7P: unlock(&m);
| m কার হাতে | |
| full | |
| not_empty-এর সারি | |
| C-এর অবস্থা | |
| P-এর অবস্থা |
- C mutex নেয়। shared ডেটা (
slot,full) দেখার আগে lock — এটিই নিয়ম। এখনmC-এর হাতে, mailbox খালি (full = 0). - শর্ত মিথ্যা।
!fullসত্য, তাই C লুপে ঢুকেcond_waitডাকে। মনে রাখুন, ডাকার মুহূর্তেওmC-এর হাতে। - এক ধাপে: সারিতে ঢোকা আর mutex ছাড়া। wait-এর ভেতরে C
not_empty-এর সারিতে যায় আরmছেড়ে দেয় — দুটি একসাথে, মাঝে কোনো ফাঁক নেই। C এখন blocked, CPU নষ্ট করছে না। ঠিক এই সময় P এসে lock চাইছে। - P ঢোকে আর শর্ত সত্য করে।
mখালি বলেই P lock পেল। সেslot = 10আরfull = 1লেখে — lock ধরে, যাতে C-এর পরীক্ষার সাথে মিশে না যায়। - P signal দেয়। C সারি থেকে বেরিয়ে “চলার জন্য প্রস্তুত” হয়, কিন্তু এখনই চলতে পারে না — wait থেকে ফেরার আগে তাকে
mআবার নিতে হবে, আরmএখনো P-এর হাতে। - P unlock করে, C mutex ফিরে পায়। P বেরিয়ে যেতেই C-এর wait-এর ভেতরে lock নেওয়া সম্পূর্ণ হয়, আর wait ফেরে। ফেরার পর C আবার
m-এর মালিক — ঠিক যেমন ডাকার আগে ছিল। - while আবার দেখে, তারপর কাজ।
full = 1, তাই লুপ শেষ। Cslotথেকে 10 নেয়,full = 0করে, unlock করে। “got 10”. wait-এর আগে-পরে দুবারই C lock ধরে শর্ত দেখেছে — তাই যা দেখেছে, তা-ই সত্য।
এবার আসল প্রোগ্রাম: একটি producer থ্রেড পাঁচটি সংখ্যা (10, 20, …, 50) পাঠায়, main থ্রেড consumer হিসেবে নেয়। ঘর একটিই, তাই দুটি condition variable লাগে — not_full (producer এখানে অপেক্ষা করে) আর not_empty (consumer এখানে)। ছাপা হয় lock ধরে, শর্ত বদলানোর মুহূর্তেই, তাই আউটপুটের ক্রম প্রতিবার একই:
#include <pthread.h>
#include <stdio.h>
static int slot, full = 0; /* the mailbox: one slot */
static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
static pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
static void *producer(void *arg) {
(void)arg;
for (int i = 1; i <= 5; i++) {
pthread_mutex_lock(&m);
while (full) /* slot taken: sleep */
pthread_cond_wait(¬_full, &m);
slot = i * 10; full = 1;
printf("put %d\n", slot);
pthread_cond_signal(¬_empty); /* a letter has arrived */
pthread_mutex_unlock(&m);
}
return NULL;
}
int main(void) {
pthread_t p; int sum = 0;
pthread_create(&p, NULL, producer, NULL);
for (int k = 0; k < 5; k++) { /* main is the consumer */
pthread_mutex_lock(&m);
while (!full) /* nothing yet: sleep */
pthread_cond_wait(¬_empty, &m);
printf(" got %d\n", slot);
sum += slot; full = 0;
pthread_cond_signal(¬_full); /* the slot is free again */
pthread_mutex_unlock(&m);
}
pthread_join(p, NULL);
printf("sum = %d\n", sum);
return 0;
}একটি আসল রানের আউটপুট:
put 10
got 10
put 20
got 20
put 30
got 30
put 40
got 40
put 50
got 50
sum = 150
put আর got কঠোরভাবে পালা করে আসে — এবং এটি ভাগ্য নয়, প্রমাণ করা যায়। put k ছাপা হয় full = 1 করার সময়, lock ধরে; পরের put-এর আগে producer-কে full == 0 দেখতে হবে, যা কেবল got k-এর পরেই সম্ভব। আবার got k-এর জন্য full == 1 চাই, যা put k-এর পরেই আসে। তাই ক্রম একটিই: put 10, got 10, put 20, … — যদিও কোন থ্রেড কতবার ঘুমিয়েছে, তা রান-ভেদে আলাদা (সেটি ছাপা হয়নি)। যোগফল ১০ + ২০ + ৩০ + ৪০ + ৫০ = ১৫০. দুটি আলাদা condition variable রাখায় producer-এর signal শুধু consumer-কে জাগায়, আর consumer-এরটি শুধু producer-কে — কেউ অকারণে জাগে না।
প্রতিটি condition variable-এর জন্য বলতে পারতে হবে: (১) কোন mutex তার সঙ্গী — সব wait একই mutex দিয়ে, (২) কোন শর্তের জন্য অপেক্ষা — শর্তটি সেই mutex-রক্ষিত ডেটা দিয়ে লেখা, (৩) কে শর্তটি সত্য করে, আর সে কি lock ধরে বদলায় ও তারপর signal দেয়। এই তিনটির একটিও অস্পষ্ট হলে কোডে বাগ আছে ধরে নিন।
signal না broadcast? signal সস্তা — একজনকেই জাগায়। কিন্তু তিন অবস্থায় broadcast লাগে: (ক) একটি পরিবর্তনে একাধিক অপেক্ষাকারী এগোতে পারে (যেমন shutdown, বা অংশ ৮-এর latch); (খ) একই condition variable-এ ভিন্ন ভিন্ন শর্তের থ্রেড অপেক্ষা করে — signal ভুল জনকে জাগালে সে আবার ঘুমায় আর সঠিকজন ঘুমিয়েই থাকে; (গ) কে এগোতে পারবে আপনি জানেন না (অংশ ৭-এর ভিন্ন আকারের বরাদ্দ)। while লুপ থাকলে signal-এর জায়গায় broadcast দেওয়া সবসময় নিরাপদ — শুধু ধীর, কারণ সবাই জেগে lock-এর জন্য লাইন দেয় আর বেশিরভাগ আবার ঘুমায়। Lampson আর Redell এমন অবস্থাকে বলেছিলেন covering condition.
৩ · কেন সবসময় while — spurious wakeup আর “চুরি যাওয়া” জাগরণ
নতুনরা প্রায়ই লেখেন if (!full) wait(...) — যুক্তি সহজ: “signal তো তখনই আসে যখন শর্ত সত্য হয়েছে, তাহলে আবার দেখব কেন?” উত্তর: signal আসার মুহূর্তে শর্ত সত্য ছিল, কিন্তু wait থেকে ফেরার মুহূর্তে সত্য থাকবে — এমন কোনো প্রতিশ্রুতি pthreads দেয় না। দুটি কারণ:
- Spurious wakeup: POSIX স্পষ্টভাবে অনুমতি দেয় যে কোনো signal ছাড়াই
pthread_cond_waitফিরতে পারে। আরpthread_cond_signal-এর সংজ্ঞাও “অন্তত একজনকে জাগাবে” — multiprocessor-এ দ্রুত বাস্তবায়নের জন্য কখনো দুজনও জাগতে পারে। Linux-এ বাস্তবে কারণ হতে পারে signal handler-এর বাধা বা futex-এর ভেতরের দৌড়। Windows-এরCONDITION_VARIABLE-এর ডকুমেন্টেশনও একই সতর্কবার্তা দেয়, আর Java-রObject.wait-এরও। - চুরি যাওয়া জাগরণ (stolen wakeup): signal পেয়ে জেগে ওঠা থ্রেড সঙ্গে সঙ্গে চলে না — তাকে mutex-এর জন্য লাইনে দাঁড়াতে হয়। সেই ফাঁকে অন্য একটি থ্রেড, যে সবে এসেছে আর কখনো ঘুমায়ইনি, mutex পেয়ে শর্তটি ভোগ করে ফেলতে পারে। এটিই Mesa semantics-এর স্বাভাবিক ফল (অংশ ৬)।
দ্বিতীয়টি কীভাবে ঘটে, ধাপে ধাপে দেখুন। দুজন consumer (C1, C2), একজন producer (P), আর একটিমাত্র item:
1consumer: lock(&m);2 if (items == 0) // BUG: if, not while3 cond_wait(&cv, &m);4 items--; take(); unlock(&m);5producer: lock(&m); items++;6 cond_signal(&cv); unlock(&m);
| items | |
| m কার হাতে | |
| cv-এর সারি | |
| C1 | |
| C2 |
- C1 দেখে খালি, ঘুমায়। items = 0, তাই C1
cond_wait-এ mutex ছেড়েcv-এর সারিতে ঘুমায়। - P item রাখে আর signal দেয়। items = 1. signal C1-কে সারি থেকে বের করে, কিন্তু mutex এখনো P-এর হাতে — C1 জেগেছে, চলতে পারছে না।
- P বেরোয়; দুজন mutex চায়। ঠিক এই মুহূর্তে C2 নামের নতুন consumer এসে
lock(&m)ডাকে। এখন C1 (wait-এর ভেতরে) আর C2 — দুজনেই mutex-এর প্রার্থী। Pthreads কোনো ক্রম ঠিক করে দেয় না: জাগা থ্রেড আগে পাবে, এমন কোনো নিয়ম নেই। - C2 জেতে আর item নেয়। C2 দেখে items = 1, wait-ই করে না — item নিয়ে items = 0 করে বেরিয়ে যায়। C1-এর জন্য যে item এসেছিল, তা “চুরি” হয়ে গেল।
- C1 mutex পায়, wait ফেরে। এখন items = 0. প্রশ্ন একটাই: C1 কি শর্তটি আবার দেখবে?
ifহলে: খালি থেকে নেওয়া।ifএকবারই দেখেছিল, তাই C1 সোজাitems--করে: items = −1. বাস্তবের queue-তে এর মানে খালি queue থেকে আবর্জনা পড়া, NULL pointer, বা অন্যের মেমরি নষ্ট — আর বাগটি ঘটে কালেভদ্রে, শুধু ভারী load-এ।whileহলে: আবার দেখা, আবার ঘুম। লুপ items = 0 দেখে C1-কে আবার ঘুম পাড়ায়। পরের item এলে সে নেবে। কোনো ক্ষতি হয়নি — শুধু একবার অকারণে জাগা। spurious wakeup হলেও ঠিক এই একই রক্ষা কাজ করে।
while কেন যথেষ্ট — একটি ছোট যুক্তি
বিশ্ববিদ্যালয়ের পরীক্ষায় প্রায়ই জিজ্ঞেস করা হয়, while লুপ “কেন সঠিক”। যুক্তিটি loop invariant দিয়ে:
- দাবি:
while (!B) wait(cv, m);লুপ থেকে বেরোনোর মুহূর্তে থ্রেডটিmধরে আছে আর B সত্য। - প্রমাণ: লুপের প্রতিটি পরীক্ষার সময় থ্রেড
mধরে থাকে — লুপের আগে lock নেওয়া হয়েছে, আর wait ফেরার আগে lock আবার নেয়। লুপ থেকে বেরোনো যায় শুধু একটি পথে: পরীক্ষায় B সত্য পাওয়া। পরীক্ষা আর বেরোনোর মাঝে lock ছাড়া হয়নি, আর B শুধুm-রক্ষিত ডেটা দিয়ে লেখা, তাই অন্য কেউ এর মধ্যে B বদলাতে পারেনি। কতবার, কেন বা কে জাগিয়েছে — কোনোটিই যুক্তিতে লাগেনি। ∎ - ফল: while থাকলে নিরাপত্তা (safety) signal-এর ওপর নির্ভর করে না; signal শুধু অগ্রগতির (liveness) জন্য — B সত্য হওয়ার পর অন্তত একজন অপেক্ষাকারীকে কেউ জাগাবে। তাই বাড়তি signal বা broadcast কখনো ভুল ঘটায় না, আর signal কম পড়লে ঘটে শুধু চিরকালের ঘুম। Lampson আর Redell ঠিক এই অর্থে signal-কে বলেছিলেন একটি “ইঙ্গিত” (hint).
৪ · Lost wakeup — শর্ত আর ঘুমের মাঝের ফাঁক
while লুপ সঠিক, কিন্তু শুধু তখনই, যখন শর্ত পরীক্ষা আর শর্ত বদলানো দুটোই একই mutex ধরে হয়। কেউ “গতি বাড়াতে” lock ছাড়াই শর্তটি দেখলে বা বদলালে একটি সূক্ষ্ম বাগ জন্মায়: lost wakeup. মনে আছে, condition variable-এর স্মৃতি নেই? কেউ সারিতে ঢোকার আগেই signal এসে গেলে সেটি হারিয়ে যায়, আর পরে ঘুমানো থ্রেডটিকে জাগানোর আর কেউ থাকে না।
/* waiter: checks the flag WITHOUT the lock */
while (!ready) { /* (1) reads ready == 0 */
pthread_mutex_lock(&m); /* ... preempted here ... */
pthread_cond_wait(&cv, &m); /* (4) sleeps -- forever? */
pthread_mutex_unlock(&m);
}
/* setter: changes the flag WITHOUT the lock */
ready = 1; /* (2) */
pthread_cond_signal(&cv); /* (3) nobody is waiting yet */1W: while (!ready) { // no lock!2W: lock(&m);3W: cond_wait(&cv, &m);4S: ready = 1; // no lock!5S: cond_signal(&cv);6// semaphore: V(s) is remembered
| ready | |
| cv-এর সারি | |
| W (অপেক্ষাকারী) | |
| S (সেটার) |
- W lock ছাড়া ready পড়ে। ready = 0, তাই W লুপের ভেতরে যাবে — এই সিদ্ধান্ত হয়ে গেছে, কিন্তু W এখনো সারিতে ঢোকেনি।
- ঠিক এখানে W preempted. timer interrupt-এ W CPU হারায় (বা অন্য core-এ S একই সময়ে চলে)। S
ready = 1লেখে। W-এর সিদ্ধান্ত এখন পুরোনো তথ্যের ওপর দাঁড়িয়ে। - S signal দেয় — কেউ নেই।
cv-এর সারি খালি, তাই signal কিছুই করে না, কোথাও লেখাও থাকে না। জাগানোর একমাত্র সুযোগটি হারাল। - W ফিরে এসে ঘুমায়। W lock নিয়ে
cond_waitডাকে — অথচ ready ইতিমধ্যে 1. wait-এর ভেতরে শর্ত দেখা হয় না; W সারিতে ঢুকে ঘুমায়। - চিরকালের ঘুম। S-এর কাজ শেষ, আর কেউ signal দেবে না। প্রোগ্রাম আটকে গেল — কোনো error নেই, CPU-ও খরচ হচ্ছে না, শুধু কিছুই এগোচ্ছে না। এমন বাগ লাখে একবার ঘটতে পারে, ঠিক production-এর ভিড়ের সময়।
- সমাধান: পরীক্ষা আর বদল দুটোই lock ধরে। W:
lock; while (!ready) wait; unlock. S:lock; ready = 1; signal; unlock. এখন S ready বদলাতে পারে কেবল W lock না ধরার সময় — অর্থাৎ হয় W পরীক্ষার আগে (W তখন ready = 1 দেখে ঘুমাবেই না), নয় W সারিতে ঢুকে mutex ছাড়ার পরে (তখন signal W-কে পায়)। wait-এর “সারিতে ঢোকা আর mutex ছাড়া একসাথে” নিশ্চয়তা মাঝের ফাঁকটি বন্ধ করে। - তুলনা: semaphore মনে রাখে। একই ক্রমে semaphore দিলে S-এর
V(s)মান 0 থেকে 1 করে রাখে; W পরেP(s)ডাকলে 1 থেকে 0 করে না ঘুমিয়েই এগোয়। এজন্য semaphore-এ lost wakeup হয় না — কিন্তু condition variable-কে সেই স্মৃতি দেয় শুধু mutex-রক্ষিত শর্ত ভ্যারিয়েবলটি।
signal কি lock ধরে দেবেন, না ছেড়ে?
এখানে একটি সূক্ষ্ম পার্থক্য আছে। শর্ত বদলানো lock ধরে করা বাধ্যতামূলক — নইলে উপরের বাগ। কিন্তু signal নিজে unlock-এর আগে না পরে দেবেন, তা নিয়ে POSIX নমনীয়: সঠিকতার জন্য দুটিই চলে, শুধু শর্ত বদলানো lock ধরে হলেই। POSIX-এর ভাষায় mutex ধরা না থাকলেও signal ডাকা যায়, তবে “পূর্বানুমেয় scheduling আচরণ” চাইলে mutex ধরে ডাকতে হবে। দুই পক্ষের যুক্তি:
lock ধরে signal
সবচেয়ে নিরাপদ অভ্যাস, ভুল হওয়ার সুযোগ কম। জাগা থ্রেড সঙ্গে সঙ্গে mutex-এ আটকে যায় (“hurry up and wait”), তবে glibc-র মতো বাস্তবায়ন এই খরচ অনেকটা কমায়। Java আর Python এটিকেই বাধ্যতামূলক করেছে।
ডিফল্ট পছন্দunlock-এর পরে signal
জাগা থ্রেড সঙ্গে সঙ্গে mutex পেতে পারে, তাই কিছুটা দ্রুত হতে পারে। ঝুঁকি: unlock আর signal-এর মাঝে অন্য কেউ ঢুকে শর্ত ভোগ করতে পারে, আর condition variable ধ্বংস করার মতো কাজে ক্রমের ভুলে বাগ হয়। এটি মাপা অপ্টিমাইজেশন, অভ্যাস নয়।
মেপে তবেই“ready তো একটি int, পড়া-লেখা তো atomic” — এই যুক্তি ভুল। সমস্যা একটি পড়ার অবিভাজ্যতা নয়; সমস্যা “পড়া, সিদ্ধান্ত নেওয়া, ঘুমানো” — এই তিন ধাপের মাঝে ফাঁক। ready-কে volatile বা C11 _Atomic করলেও ফাঁক থাকে। আর lock ছাড়া সাধারণ ভ্যারিয়েবল পড়া-লেখা C-তে data race, তাই undefined behavior (পর্ব ১৫)।
৫ · Monitor — mutual exclusion ভেতরেই বাঁধা
এতক্ষণ mutex আর condition variable আলাদা দুটি জিনিস ছিল, আর সঠিকভাবে জোড়া লাগানোর দায়িত্ব ছিল প্রোগ্রামারের। Monitor সেই দায়িত্ব ভাষা বা compiler-এর হাতে তুলে দেয়। একটি monitor হলো একটি মডিউল বা class, যার মধ্যে থাকে:
- shared ডেটা — যা বাইরের কেউ সরাসরি ছুঁতে পারে না;
- procedure (method) — ডেটা ছোঁয়ার একমাত্র পথ; আর নিয়ম: যেকোনো মুহূর্তে monitor-এর ভেতরে সর্বোচ্চ একটি থ্রেড সক্রিয় — compiler নিজেই প্রতিটি procedure-এর শুরুতে lock আর শেষে unlock বসায়;
- condition variable — monitor-এর ভেতরে অপেক্ষার জন্য,
x.wait()আরx.signal(); - শুরুর কোড — ডেটার প্রাথমিক মান।
ঢুকতে চাওয়া কিন্তু অপেক্ষারত থ্রেডরা থাকে monitor-এর দরজার entry queue-তে; প্রতিটি condition variable-এর আলাদা সারি থাকে। x.wait() ডাকা থ্রেড monitor ছেড়ে x-এর সারিতে যায়, ফলে আরেকজন ঢুকতে পারে। Pseudocode-এ (Pascal-ঘেঁষা, পাঠ্যবইয়ের রীতিতে) এক-ঘরের mailbox:
monitor Mailbox {
int slot; bool full = false; // shared data: only reachable inside
condition notFull, notEmpty;
procedure put(int v) { // compiler adds: enter (lock)
while (full) notFull.wait();
slot = v; full = true;
notEmpty.signal();
} // compiler adds: leave (unlock)
procedure get() returns int {
while (!full) notEmpty.wait();
full = false; notFull.signal();
return slot;
}
}অংশ ২-এর C প্রোগ্রামের সাথে তুলনা করুন: lock/unlock উধাও, slot আর full বাইরে থেকে ছোঁয়া যায় না, আর condition variable ঘোষণা হয়েছে ঠিক যে ডেটার সাথে সম্পর্কিত তার পাশে। সব সমন্বয়ের কোড এক জায়গায় — এক নজরে যাচাই করা যায়। অংশ ১-এর semaphore-এর সমস্যাগুলোর মধ্যে “lock ভুলে যাওয়া” আর “ভুল জায়গায় unlock” এখানে আর সম্ভবই নয়।
ব্যাংকের লকার-রুম ভাবুন। ঘরে ঢোকার একটিই দরজা, আর দারোয়ান একবারে একজনকেই ঢুকতে দেন — কেউ চাইলেও নিয়ম ভাঙতে পারেন না (built-in mutual exclusion). দরজার বাইরে লাইন (entry queue). ভেতরে যিনি আছেন, তাঁর লকারের চাবি এখনো তৈরি হয়নি — তিনি পাশের অপেক্ষা-ঘরে যান (condition-এর সারি), আর তখন লাইনের পরের জন ঢোকেন। চাবি তৈরি হলে কর্মকর্তা অপেক্ষা-ঘরের মানুষটিকে ডাকেন (signal). প্রশ্ন হলো, ডাক দেওয়ার পর কর্মকর্তা নিজে ঘরে থেকে যাবেন, না জায়গা ছেড়ে দেবেন? এর উত্তরই Hoare আর Mesa-র পার্থক্য।
Monitor invariant
Monitor নিয়ে যুক্তি দেওয়ার মূল হাতিয়ার একটি monitor invariant — shared ডেটা নিয়ে এমন একটি শর্ত, যা monitor-এর ভেতরে কেউ না থাকলে সবসময় সত্য। mailbox-এ: “full সত্য হলে slot-এ একটি বৈধ, এখনো না-নেওয়া মান আছে”। নিয়ম হলো, প্রতিটি থ্রেড monitor ছাড়ার আগে — procedure-এর শেষে এবং wait() ডাকার আগে — invariant আবার সত্য করবে, কারণ wait-এর মুহূর্তে অন্য কেউ ঢুকবে। ভেতরে কাজের মাঝখানে invariant সাময়িক ভাঙতে পারে; কেউ তো দেখছে না।
ধারণাটির শিকড় ১৯৭১–৭৩ সালে। Edsger Dijkstra “secretary” নামে একটি কাঠামোর কথা বলেছিলেন। Per Brinch Hansen ১৯৭৩ সালে তাঁর বই Operating System Principles-এ “shared class” নামে monitor-এর ধারণা আর notation দেন। C. A. R. Hoare ১৯৭৪ সালের অক্টোবরে Communications of the ACM-এ প্রকাশ করেন “Monitors: An Operating System Structuring Concept” — নাম, condition variable-এর সুনির্দিষ্ট semantics, আর semaphore দিয়ে monitor বানানোর প্রমাণ সেখানেই। Hoare নিজেই Brinch Hansen-এর কৃতিত্ব স্বীকার করেন। ১৯৭৫ সালে Brinch Hansen-এর Concurrent Pascal হয় monitor-ওয়ালা প্রথম প্রোগ্রামিং ভাষা, আর তা দিয়ে লেখা হয় Solo নামের একটি ছোট OS.
৬ · Hoare বনাম Mesa — signal দিলে কে চলবে?
ধরুন থ্রেড P monitor-এর ভেতরে, আর x-এর সারিতে থ্রেড C ঘুমিয়ে। P x.signal() ডাকল। monitor-এর নিয়ম বলে ভেতরে একজনই সক্রিয় থাকবে — তাহলে কে? দুটি উত্তর, আর তাদের ফল অনেক দূর পর্যন্ত যায়।
| বিষয় | Hoare (signal-and-wait), ১৯৭৪ | Mesa (signal-and-continue), ১৯৮০ |
|---|---|---|
| signal-এর পর কে চলে | C সঙ্গে সঙ্গে; P থেমে যায় একটি বিশেষ urgent queue-তে | P চলতে থাকে; C শুধু “চলার যোগ্য” হয়ে আবার monitor-এর লাইনে যায় |
| C যখন চলে, শর্ত কি সত্য? | হ্যাঁ, নিশ্চিত — মাঝে কেউ ঢুকতে পারেনি | নিশ্চয়তা নেই — P বা লাইনের অন্য কেউ আগে শর্ত বদলাতে পারে |
| wait-এর চারপাশে | if যথেষ্ট | while বাধ্যতামূলক |
| signal দেওয়ার আগে P-এর দায় | invariant আর C-এর শর্ত দুটোই সত্য করা, কারণ এখনই ভেতরে C আসবে | শুধু শর্তটি সম্ভবত সত্য — ইঙ্গিত; invariant লাগে কেবল monitor ছাড়ার সময় |
| P কখন ফেরে | C monitor ছাড়লে বা আবার wait করলে, entry queue-র আগে | P থামেই না |
| Context switch | বেশি: প্রতিটি signal-এ দুবার (P→C, পরে C→P) | কম: C চলে P বের হওয়ার পর |
| broadcast, timeout | স্বাভাবিকভাবে মেলে না — সবাই একসাথে “এখনই” চলতে পারে না | সহজ: broadcast সবাইকে লাইনে পাঠায়; timeout-এ জাগা থ্রেডও while-এ আবার দেখে |
| কোথায় দেখা যায় | পাঠ্যবই, প্রমাণ, কিছু গবেষণা-ভাষা | pthreads, Java, C#, Go, Python, Windows, Linux kernel — প্রায় সবখানে |
একটি তৃতীয় রূপও আছে, Brinch Hansen-এর Concurrent Pascal-এ: signal হবে procedure-এর শেষ কাজ, আর signal দিয়েই P monitor ছেড়ে যায় (“signal-and-exit” বা signal-and-return). এতে Hoare-এর নিশ্চয়তা থাকে, কিন্তু urgent queue লাগে না। নিচে একই ঘটনা দুই semantics-এ পাশাপাশি দেখুন — C1 অপেক্ষায়, P একটি item দিয়ে signal দেয়, আর C2 দরজার লাইনে:
1C1, C2: while/if (items == 0) x.wait(); items--;2P: items++; x.signal();3P: ... more work inside ...; leave4arrival order at the door: C1, P, C2
| Hoare: ভেতরে | |
| Hoare: urgent │ entry | |
| Mesa: ভেতরে | |
| Mesa: entry | |
| x-এর সারি (H │ M) | |
| items (H │ M) |
- C1 ঢোকে, খালি দেখে, wait করে। দুই semantics-এ একই: C1 x-এর সারিতে, monitor খালি, দরজায় P আর C2.
- P ঢোকে, item রাখে। items = 1. এখনো দুই দিকে সব একই। এবার P ডাকবে
x.signal(). - signal — এখানেই দুই পথ আলাদা। Hoare: P সঙ্গে সঙ্গে থেমে urgent queue-তে, monitor সরাসরি C1-এর হাতে। Mesa: C1 x-এর সারি থেকে বেরিয়ে দরজার লাইনের শেষে দাঁড়ায় — C2-এর পেছনে; P ভেতরেই চলতে থাকে।
- Hoare-এ C1 item পায়; Mesa-য় P কাজ শেষ করে। Hoare: C1 items = 1 দেখে নেয় (items = 0) আর বেরিয়ে যায় — শর্তটি নিশ্চিতভাবে সত্য ছিল, কারণ signal থেকে C1 পর্যন্ত কেউ ঢোকেনি। Mesa: P বাকি কাজ সেরে বেরোয়; items এখনো 1, কিন্তু C1 এখনো ভেতরে আসেনি।
- Hoare-এ P ফেরে; Mesa-য় C2 আগে। Hoare: urgent queue-র অগ্রাধিকার entry queue-র ওপরে, তাই C2-এর আগে P ফিরে কাজ শেষ করে। Mesa: লাইনের প্রথমে C2 — সে items = 1 দেখে নিয়ে নেয় (items = 0). C1-কে দেওয়া “ইঙ্গিত” মিথ্যা হয়ে গেল।
- শেষ জন ঢোকে। Hoare: C2 items = 0 দেখে x.wait করে — পরের item-এর অপেক্ষায়, যা সঠিক। Mesa: C1 অবশেষে ভেতরে।
ifহলে সে আর দেখে না, items = −1 — বাগ।whileহলে items = 0 দেখে আবার ঘুমায় — সঠিক। - হিসাব। monitor-এর মালিকদের ক্রম Hoare-এ C1, P, C1, P, C2 — ৪ বার হাতবদল; Mesa-য় C1, P, C2, C1 — ৩ বার। Hoare নিশ্চয়তা দেয় বাড়তি context switch-এর বিনিময়ে; Mesa সস্তা, কিন্তু প্রতিটি wait-কে while দিয়ে নিজেকে রক্ষা করতে হয়। দুই দিকেই শেষে একজন অপেক্ষায় — শুধু কে, তা আলাদা।
একই ঘটনা নিচের ছোট simulator-এ চালিয়ে দেখুন। প্রতিটি থ্রেড একটি JavaScript generator: yield 'wait' বা yield 'signal' মানে monitor-এর কাছে অনুরোধ, আর run() নিয়ম অনুযায়ী ঠিক করে পরের জন কে। তিনটি সংমিশ্রণ চলে — Hoare + if, Mesa + if, Mesa + while:
// Monitor simulator: C1 waits for an item, P puts 1 item and signals, C2 queues at the door.
function* consumer(me, S, useWhile) {
if (useWhile) { while (S.items === 0) { S.say(me + ' sees items=0, waits'); yield 'wait'; } }
else if (S.items === 0) { S.say(me + ' sees items=0, waits'); yield 'wait'; }
S.items--; S.say(me + ' takes item -> items=' + S.items + (S.items < 0 ? ' BUG!' : ''));
}
function* producer(S) {
S.items++; S.say('P puts item -> items=' + S.items + ', signal(x)');
yield 'signal';
S.say('P continues inside the monitor');
}
function run(sem, useWhile) {
const S = { items: 0, out: [], say(m) { this.out.push(' ' + m); } };
const g = { C1: consumer('C1', S, useWhile), P: producer(S), C2: consumer('C2', S, useWhile) };
const entry = ['C1', 'P', 'C2'], urgent = [], condQ = [];
let owner = entry.shift();
while (owner) {
S.out.push('[' + owner + ' runs] entry=' + (entry.join(',') || '-') + ' urgent=' + (urgent.join(',') || '-') + ' x=' + (condQ.join(',') || '-'));
const act = g[owner].next().value; // run owner until its next monitor action
if (act === 'signal' && condQ.length) {
const w = condQ.shift();
if (sem === 'Hoare') { urgent.push(owner); owner = w; continue; } // signal-and-wait
entry.push(w); // Mesa: back to the door
}
if (act === 'signal') { continue; } // signaller keeps the monitor
if (act === 'wait') condQ.push(owner); // release monitor, sleep on x
owner = urgent.shift() || entry.shift(); // leave: urgent first, then the entry queue
}
console.log('== ' + sem + ' + ' + (useWhile ? 'while' : 'if') + ' ==');
console.log(S.out.join('\n') + '\n end: items=' + S.items + ', still waiting on x: ' + (condQ.join(',') || 'none'));
}
run('Hoare', false); run('Mesa', false); run('Mesa', true);আউটপুট:
== Hoare + if ==
[C1 runs] entry=P,C2 urgent=- x=-
C1 sees items=0, waits
[P runs] entry=C2 urgent=- x=C1
P puts item -> items=1, signal(x)
[C1 runs] entry=C2 urgent=P x=-
C1 takes item -> items=0
[P runs] entry=C2 urgent=- x=-
P continues inside the monitor
[C2 runs] entry=- urgent=- x=-
C2 sees items=0, waits
end: items=0, still waiting on x: C2
== Mesa + if ==
[C1 runs] entry=P,C2 urgent=- x=-
C1 sees items=0, waits
[P runs] entry=C2 urgent=- x=C1
P puts item -> items=1, signal(x)
[P runs] entry=C2,C1 urgent=- x=-
P continues inside the monitor
[C2 runs] entry=C1 urgent=- x=-
C2 takes item -> items=0
[C1 runs] entry=- urgent=- x=-
C1 takes item -> items=-1 BUG!
end: items=-1, still waiting on x: none
== Mesa + while ==
[C1 runs] entry=P,C2 urgent=- x=-
C1 sees items=0, waits
[P runs] entry=C2 urgent=- x=C1
P puts item -> items=1, signal(x)
[P runs] entry=C2,C1 urgent=- x=-
P continues inside the monitor
[C2 runs] entry=C1 urgent=- x=-
C2 takes item -> items=0
[C1 runs] entry=- urgent=- x=-
C1 sees items=0, waits
end: items=0, still waiting on x: C1
প্রতিটি [X runs] লাইন দেখায় কে monitor পেল, আর তখন দরজার লাইন (entry), Hoare-এর urgent আর x-এর সারি। Hoare + if: signal-এর পরেই [C1 runs] ... urgent=P — P সরে গেছে, C1 item পেয়েছে, তারপর P, শেষে C2 অপেক্ষায়। if থাকা সত্ত্বেও ফল সঠিক। Mesa + if: signal-এর পর P-ই আবার চলে (entry=C2,C1), C2 আগে ঢুকে item নেয়, আর C1 না দেখে নেয় — items=-1 BUG!. Mesa + while: একই ক্রম, কিন্তু C1 আবার দেখে ঘুমায়; শেষে items=0. simulator বদলে দেখুন: arrival order-এ C2-কে বাদ দিলে Mesa + if-ও “ঠিক” চলে — ঠিক এজন্যই এই বাগ টেস্টে ধরা পড়ে না।
আরও গভীরে: Linux kernel-এ condition variable-এর কাজ করে wait queue: wait_event(wq, cond) macro ভেতরে নিজেই একটি লুপে শর্ত দেখে, থ্রেডকে সারিতে রেখে state বদলায়, তারপর আবার শর্ত দেখে তবে ঘুমায় — lost wakeup ঠেকানোর ঠিক সেই ক্রম; আর wake_up(&wq) জাগায়। এক-বারের ঘটনার জন্য আছে struct completion (complete/wait_for_completion), যা আসলে একটি latch. User space-এ glibc-র pthread_cond_t futex-এর ওপর বানানো; পুরোনো বাস্তবায়নে একটি ক্রম-সংক্রান্ত বাগ (wait শুরুর আগে দেওয়া signal-ও কখনো ভোগ করা যেত) সারাতে glibc 2.25 (২০১৭)-এ condition variable-এর অ্যালগরিদম নতুন করে লেখা হয়।
৭ · Semaphore দিয়ে monitor বানানো আর resource allocator
Hoare-এর ১৯৭৪ সালের প্রবন্ধের একটি বড় দাবি ছিল: monitor semaphore-এর চেয়ে বেশি শক্তিশালী নয়, শুধু বেশি কাঠামোবদ্ধ — কারণ semaphore দিয়েই monitor বানানো যায়। পাঠ্যবইয়ে (Silberschatz-দের Operating System Concepts-এ) এই নির্মাণটি Hoare semantics-এর জন্য দেওয়া আছে, আর পরীক্ষায় এটি বারবার আসে। উপাদান পাঁচটি:
| উপাদান | শুরুর মান | কাজ |
|---|---|---|
mutex (semaphore) | 1 | monitor-এর দরজা — এটি দিয়েই “ভেতরে একজন”; এর সারিই entry queue |
next (semaphore) | 0 | signal দিয়ে থেমে যাওয়া থ্রেডরা এখানে ঘুমায় — Hoare-এর urgent queue |
next_count (int) | 0 | next-এ কতজন ঘুমিয়ে |
x_sem (প্রতিটি condition x-এর জন্য) | 0 | x-এর সারি |
x_count (int) | 0 | x-এ কতজন ঘুমিয়ে |
/* every monitor procedure F becomes: */
wait(mutex);
... body of F ...
if (next_count > 0) signal(next); /* a signaller is parked: let it back in */
else signal(mutex); /* otherwise open the door */
/* x.wait() */
x_count++;
if (next_count > 0) signal(next); /* hand the monitor to a parked signaller */
else signal(mutex);
wait(x_sem); /* sleep on x */
x_count--;
/* x.signal() */
if (x_count > 0) { /* no waiter: do nothing */
next_count++;
signal(x_sem); /* wake one waiter on x ... */
wait(next); /* ... and park myself (urgent) */
next_count--;
}কেন্দ্রীয় কৌশলটির নাম baton passing: monitor থেকে বেরোনোর সময় থ্রেড “দরজা খুলে দেয়” না, বরং ব্যাটনটি (ভেতরে থাকার অধিকার) সরাসরি একজনের হাতে তুলে দেয় — আগে next-এ পার্ক করা কাউকে, কেউ না থাকলে তবেই mutex খুলে দরজার লাইনকে। x.signal-এ ব্যাটন যায় x-এর অপেক্ষাকারীর হাতে, mutex-এ হাত না দিয়েই। ফলে ব্যাটন যখন যেখানে যায়, mutex-এর মান 0-ই থাকে, আর লাইনের কেউ ফাঁক গলে ঢুকতে পারে না। এটিই Hoare-এর নিশ্চয়তা বাস্তবায়ন করে। ধাপে ধাপে দেখুন — C1 একটি procedure-এ x.wait() করে, P আরেকটিতে x.signal():
1F(): wait(mutex);2 body;3 if (next_count>0) signal(next); else signal(mutex);4x.wait: x_count++;5 if (next_count>0) signal(next); else signal(mutex);6 wait(x_sem); x_count--;7x.signal: if (x_count>0) {8 next_count++; signal(x_sem);9 wait(next); next_count--; }
| mutex | |
| next │ next_count | |
| x_sem │ x_count | |
| ভেতরে (ব্যাটন) | |
| ঘুমিয়ে |
- C1 procedure-এ ঢোকে।
wait(mutex): 1 → 0. ব্যাটন C1-এর হাতে। - C1
x.wait()ডাকে।x_count= 1. কেউ পার্ক করা নেই (next_count= 0), তাইsignal(mutex): 0 → 1 — দরজা খুলল। তারপরwait(x_sem): মান 0, তাই C1 ঘুমায়। ভেতরে কেউ নেই। - P procedure-এ ঢোকে।
wait(mutex): 1 → 0. ব্যাটন P-এর হাতে। - P
x.signal()ডাকে।x_count= 1 > 0, তাইnext_count= 1,signal(x_sem)C1-কে জাগায় (একজন ঘুমিয়ে ছিল, তাই মান 0-ই থাকে), আরwait(next)-এ P নিজে পার্ক হয়। mutex-এ কেউ হাত দেয়নি — এখনো 0. ব্যাটন সরাসরি C1-এর হাতে: Hoare-এর “signal-and-wait”. - C1 ফেরে, কাজ শেষে বেরোয়।
wait(x_sem)-এর পরের লাইন:x_count= 0. procedure-এর শেষেnext_count= 1 > 0, তাইsignal(mutex)নয়,signal(next)— ব্যাটন পার্ক করা P-এর হাতে। দরজার লাইনের কেউ থাকলেও ঢুকতে পারত না। - P ফেরে আর বেরোয়।
wait(next)-এর পরেnext_count= 0. P-এর procedure শেষ, কেউ পার্ক নেই, তাই এবারsignal(mutex): 0 → 1 — দরজা খুলল। - Invariant মিলিয়ে নিন। সবসময় “mutex = 1” আর “কেউ ভেতরে আছে” একসাথে কখনো সত্য হয়নি; ব্যাটন ছিল ঠিক একজনের হাতে বা দরজায়। শেষে সব মান শুরুর মতো। Mesa semantics বানাতে
nextআরnext_countলাগে না:x.signalশুধুif (x_count > 0) signal(x_sem), আর জাগা থ্রেডwait(mutex)দিয়ে আবার লাইনে দাঁড়ায় — তখন while বাধ্যতামূলক।
Resource allocator — অগ্রাধিকারসহ অপেক্ষা
Hoare-এর প্রবন্ধে আরেকটি ধারণা ছিল conditional wait (scheduled wait): x.wait(c), যেখানে c একটি অগ্রাধিকার-সংখ্যা। x.signal() জাগায় সবচেয়ে ছোট সংখ্যার থ্রেডকে। পাঠ্যবইয়ের উদাহরণ: একটি সম্পদ (ধরুন একটি প্রিন্টার বা একটি টেপ ড্রাইভ) একবারে একজনকে দেওয়া হবে, আর যে যত কম সময় ব্যবহার করবে বলে জানায়, সে তত আগে পাবে — পর্ব ১৭-এর SJF-এর মতো, যা গড় অপেক্ষা কমায়।
monitor ResourceAllocator {
bool busy = false;
condition x;
procedure acquire(int time) { /* time = how long I will use it */
if (busy) x.wait(time); /* sleep, ordered by time */
busy = true;
}
procedure release() {
busy = false;
x.signal(); /* wakes the smallest 'time' */
}
}
/* every user must follow: R.acquire(t); ...use...; R.release(); */লক্ষ করুন if — Hoare semantics-এ এটি বৈধ, কারণ release-এর signal-এর সাথে সাথেই জাগা থ্রেড চলে আর busy তখন false. Mesa semantics-এ এখানে while লাগবে, আর তখন অগ্রাধিকারের প্রতিশ্রুতিও দুর্বল হয়: জাগা থ্রেড দরজার লাইনে দাঁড়ানোর ফাঁকে নতুন আসা কেউ (যে কখনো ঘুমায়নি, যার time হয়তো বড়) আগে সম্পদ নিয়ে নিতে পারে। বাস্তবে এর সমাধান হলো সম্পদটি release-এর সময় সরাসরি জাগা থ্রেডের নামে বরাদ্দ করা (“hand-off”), অথবা সারির ক্রম নিজে একটি priority queue-তে রাখা। আর একটি সাবধানতা: SJF-এর মতোই বড় time-এর থ্রেড অনির্দিষ্টকাল starvation-এ পড়তে পারে, যদি ছোট কাজ আসতেই থাকে।
এই উদাহরণ monitor-এর একটি সীমাও দেখায়: monitor শুধু নিশ্চিত করে যে acquire আর release-এর ভেতরে কেউ মিশে যাবে না। কিন্তু কেউ acquire না করেই সম্পদ ব্যবহার করলে, বা release ভুলে গেলে, বা দুবার release করলে monitor কিছু করতে পারে না — ঠিক semaphore-এর মতো দায় তখন ফিরে আসে ব্যবহারকারীর কাছে। সমাধান হলো সম্পদটিকেই monitor-এর ভেতরে রাখা, যাতে বাইরের কেউ সরাসরি ছুঁতে না পারে।
৮ · বাস্তবের ভাষায় monitor — আর barrier ও latch
আধুনিক ভাষাগুলো monitor-এর ধারণা নিয়েছে, তবে প্রায় সবাই Mesa semantics-এ, আর অনেকে কিছুটা ঢিলেঢালাভাবে। নিচে একই এক-ঘরের mailbox পাঁচটি পরিবেশে (এগুলো এই পাতায় চালানো যায় না):
class Mailbox {
private int slot;
private boolean full = false;
public synchronized void put(int v) throws InterruptedException {
while (full) wait(); // releases this object's monitor
slot = v; full = true;
notifyAll(); // one wait set for everyone: wake all
}
public synchronized int get() throws InterruptedException {
while (!full) wait();
full = false;
notifyAll();
return slot;
}
}Java-য় প্রতিটি অবজেক্টের একটি intrinsic lock (monitor lock) আছে। synchronized method বা block সেই lock নেয়, আর lock reentrant — একই থ্রেড আবার নিতে পারে। wait(), notify(), notifyAll() ডাকতে lock ধরা থাকতে হয়, নইলে IllegalMonitorStateException. দুর্বলতা: প্রতিটি অবজেক্টে condition একটিই (একটি “wait set”), তাই producer আর consumer একই সারিতে ঘুমায়। notify() ভুল পক্ষকে জাগালে বাকিরা ঘুমিয়ে থাকতে পারে — এজন্য এখানে notifyAll(). Java-র ডকুমেন্টেশন নিজেই spurious wakeup-এর কথা বলে wait-কে লুপে রাখতে বলে।
import java.util.concurrent.locks.*;
class Mailbox2 {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private int slot; private boolean full;
void put(int v) throws InterruptedException {
lock.lock();
try {
while (full) notFull.await();
slot = v; full = true;
notEmpty.signal(); // two conditions: signal() is enough
} finally { lock.unlock(); } // unlock on every path
}
}Java 5 (২০০৪)-এর java.util.concurrent (Doug Lea-র নেতৃত্বে JSR 166) দেয় স্পষ্ট Lock আর একটি lock-এর অনেকগুলো Condition — ঠিক pthreads-এর মতো। await/signal/signalAll, সঙ্গে timeout আর interrupt সামলানো। ArrayBlockingQueue ভেতরে ঠিক এভাবেই একটি lock আর notEmpty/notFull দুটি condition ব্যবহার করে। finally-তে unlock ভুলবেন না — synchronized যা নিজে করত, এখানে তা আপনার দায়িত্ব।
class Mailbox {
private readonly object gate = new object();
private int slot; private bool full;
public void Put(int v) {
lock (gate) { // Monitor.Enter / Monitor.Exit
while (full) Monitor.Wait(gate);
slot = v; full = true;
Monitor.PulseAll(gate); // like notifyAll
}
}
public int Get() {
lock (gate) {
while (!full) Monitor.Wait(gate);
full = false;
Monitor.PulseAll(gate);
return slot;
}
}
}C#-এর lock statement আসলে Monitor.Enter আর (একটি finally-তে) Monitor.Exit. অপেক্ষা Monitor.Wait, জাগানো Monitor.Pulse/PulseAll — Java-র মতোই এক অবজেক্টে একটি সারি, Mesa semantics. নতুন .NET সংস্করণে (.NET 9) শুধু mutual exclusion-এর জন্য আলাদা System.Threading.Lock type এসেছে; Wait/Pulse লাগলে তবু Monitor. অপেক্ষার অনেক কাজ আজ SemaphoreSlim, BlockingCollection বা System.Threading.Channels দিয়ে সহজে হয়।
package main
import "fmt"
func main() {
box := make(chan int) // unbuffered channel: a rendezvous
done := make(chan struct{})
go func() {
for v := range box { // receive until closed
fmt.Println("got", v)
}
close(done)
}()
for i := 1; i <= 3; i++ {
box <- i * 10 // blocks until the consumer takes it
}
close(box)
<-done
}Go-তে sync.Cond আছে — একটি Locker (L) আর Wait/Signal/Broadcast; Go-র ডকুমেন্টেশন অনুযায়ী Wait signal বা broadcast ছাড়া ফেরে না, তবু জাগার পর শর্ত মিথ্যা হতে পারে বলে লুপ রাখতে বলা হয়। কিন্তু একই ডকুমেন্টেশন বলে, সরল কাজে বেশিরভাগ ক্ষেত্রে channel ভালো। Go-র নীতিবাক্য “memory share করে যোগাযোগ নয়, যোগাযোগ করে memory share”. উপরের unbuffered channel নিজেই এক-ঘরের mailbox-এর চেয়েও কঠোর: প্রেরক আর প্রাপক একই মুহূর্তে মেলে (পর্ব ১২-এর rendezvous). আউটপুট: got 10, got 20, got 30. Channel-এর ধারণার শিকড় Hoare-এরই ১৯৭৮ সালের CSP (Communicating Sequential Processes).
import threading
class Mailbox:
def __init__(self):
self.cv = threading.Condition() # owns a lock (an RLock by default)
self.full, self.slot = False, None
def put(self, v):
with self.cv: # acquire / release
self.cv.wait_for(lambda: not self.full) # the while loop, built in
self.slot, self.full = v, True
self.cv.notify_all()
def get(self):
with self.cv:
self.cv.wait_for(lambda: self.full)
self.full = False
self.cv.notify_all()
return self.slotPython-এর threading.Condition নিজের ভেতরে একটি lock রাখে (না দিলে একটি RLock). wait_for(predicate) ভেতরেই while লুপ চালায় — ভুল করার সুযোগ কম। lock না ধরে notify ডাকলে RuntimeError. বাস্তবে প্রায়ই সরাসরি queue.Queue ব্যবহার করা হয়, যার ভেতরে ঠিক এমন Condition আছে। (GIL থাকলেও এসব লাগে — পর্ব ১৫।)
#include <windows.h>
static SRWLOCK lock = SRWLOCK_INIT;
static CONDITION_VARIABLE notEmpty = CONDITION_VARIABLE_INIT;
static int full, slot;
int get(void) {
AcquireSRWLockExclusive(&lock);
while (!full) /* spurious wakeups happen */
SleepConditionVariableSRW(¬Empty, &lock, INFINITE, 0);
full = 0;
int v = slot;
ReleaseSRWLockExclusive(&lock);
return v;
}
/* producer side: ... full = 1; WakeConditionVariable(¬Empty); */Windows Vista থেকে Win32-এ নিজস্ব CONDITION_VARIABLE আছে; সঙ্গী lock হতে পারে SRWLOCK বা CRITICAL_SECTION (SleepConditionVariableCS). জাগানো WakeConditionVariable আর WakeAllConditionVariable. নিয়ম pthreads-এর মতোই — Mesa, while লুপ। macOS-এ pthreads-এর pthread_cond_t আছে, আর Swift-এ NSCondition.
Barrier আর countdown latch — “সবাই প্রস্তুত হলে একসাথে”
অনেক সময় অপেক্ষার শর্ত হলো একটি সংখ্যা: “N জন worker প্রস্তুত হয়েছে”। ক্রিকেট ম্যাচের শুরুর মতো — দুই আম্পায়ার, এগারোজন ফিল্ডার আর দুই ব্যাটার জায়গায় না দাঁড়ালে প্রথম বল হয় না। এর জন্য দুটি পরিচিত হাতিয়ার:
- Countdown latch: একটি গণনা N থেকে শুরু; প্রত্যেকে
count_downকরে, আরawaitকরা থ্রেডরা 0 হওয়া পর্যন্ত অপেক্ষা করে। একবার 0 হলে চিরকাল খোলা — আবার ব্যবহার করা যায় না। Java-রCountDownLatch, C++20-এরstd::latch, Go-রsync.WaitGroup(প্রায় একই), Linux kernel-এরcompletion. - Barrier: N জন থ্রেড প্রত্যেকে
barrier_waitডেকে থামে; N-তম জন পৌঁছালে সবাই একসাথে ছাড়া পায়, আর barrier পরের দফার জন্য নিজেকে আবার প্রস্তুত করে (cyclic). POSIX-এরpthread_barrier_t—pthread_barrier_waitঠিক একজনকেPTHREAD_BARRIER_SERIAL_THREADফেরত দেয় (“দফার শেষে একজন সারাংশ লিখুক”), বাকিদের 0. Java-রCyclicBarrier, C++20-এরstd::barrier. সাবধান: macOS-এpthread_barrier_tনেই।
Latch বানানো যায় mutex + condition variable + একটি counter দিয়ে — আর এখানে signal নয়, broadcast বাধ্যতামূলক, কারণ একটি পরিবর্তনে (count = 0) সব অপেক্ষাকারীর এগোনোর কথা। নিচের প্রোগ্রামে দুটি latch: ready (N = 4) — main অপেক্ষা করে সব worker প্রস্তুত হওয়া পর্যন্ত; আর start (N = 1) — একটি “স্টার্টিং গান”, main একবার count_down করলেই সব worker একসাথে ছাড়া পায়। Java-র CountDownLatch-এর ডকুমেন্টেশনের পরিচিত নকশাটিই এটি:
#include <pthread.h>
#include <stdio.h>
#define N 4
struct latch { pthread_mutex_t m; pthread_cond_t cv; int count; };
static struct latch ready = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, N };
static struct latch start = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, 1 };
static int seen[N]; /* what each worker saw at the gate */
static void count_down(struct latch *l) {
pthread_mutex_lock(&l->m);
if (--l->count == 0) pthread_cond_broadcast(&l->cv); /* wake ALL waiters */
pthread_mutex_unlock(&l->m);
}
static void await_zero(struct latch *l) {
pthread_mutex_lock(&l->m);
while (l->count > 0) pthread_cond_wait(&l->cv, &l->m);
pthread_mutex_unlock(&l->m);
}
static void *worker(void *arg) {
int id = *(int *)arg;
count_down(&ready); /* "I am ready" */
await_zero(&start); /* wait for the starting gun */
pthread_mutex_lock(&ready.m);
seen[id] = N - ready.count; /* how many were ready by now */
pthread_mutex_unlock(&ready.m);
return NULL;
}
int main(void) {
pthread_t t[N]; int ids[N];
for (int i = 0; i < N; i++) { ids[i] = i; pthread_create(&t[i], NULL, worker, &ids[i]); }
await_zero(&ready);
printf("main: all %d workers ready, firing the start gate\n", N);
count_down(&start); /* one broadcast releases everyone */
for (int i = 0; i < N; i++) pthread_join(t[i], NULL);
for (int i = 0; i < N; i++) printf("worker %d passed the gate, saw %d/%d ready\n", i, seen[i], N);
return 0;
}একটি আসল রানের আউটপুট:
main: all 4 workers ready, firing the start gate
worker 0 passed the gate, saw 4/4 ready
worker 1 passed the gate, saw 4/4 ready
worker 2 passed the gate, saw 4/4 ready
worker 3 passed the gate, saw 4/4 ready
প্রতিটি worker gate পেরিয়ে দেখেছে 4/4 প্রস্তুত — অর্থাৎ কেউ আগেভাগে দৌড় শুরু করেনি। কারণ: start খোলে শুধু main-এর count_down-এ, আর main সেটি করে ready 0 হওয়ার পরেই। আউটপুট প্রতিবার একই, কারণ ছাপা হয় শুধু main থেকে, join-এর পরে। কোন worker কখন ঘুমিয়েছে বা জেগেছে, তা প্রতিবার আলাদা — হয়তো কেউ await_zero(&start)-এ পৌঁছানোর আগেই gate খুলে গেছে; তখন সে while-এর শর্ত মিথ্যা দেখে ঘুমায়ই না। broadcast-এর জায়গায় signal দিলে চারজনের মাত্র একজন জাগত — বাকি তিনজন চিরকাল ঘুমাত, আর pthread_join-এ main-ও। একটি ছোট খুঁটিনাটি: count 0-এর নিচে গেলে (বেশি count_down) এই সরল latch তা ধরে না; বাস্তবের লাইব্রেরি তা সামলায়।
POSIX barrier দিয়ে একই “সবাই একসাথে” এভাবে লেখা যায় (Linux/glibc-তে চলে):
static pthread_barrier_t b; /* in main: pthread_barrier_init(&b, NULL, 4); */
static void *phase_worker(void *arg) {
for (int round = 0; round < 3; round++) {
compute_my_part(arg, round);
int rc = pthread_barrier_wait(&b); /* nobody starts round+1 early */
if (rc == PTHREAD_BARRIER_SERIAL_THREAD)
merge_results(round); /* exactly one thread per round */
}
return NULL;
}এমন ধাপে ধাপে চলা হিসাব — যেমন আবহাওয়ার মডেলের গ্রিড, যেখানে প্রতিটি ধাপের আগে প্রতিবেশীদের আগের ধাপ শেষ হওয়া চাই — barrier-এর প্রধান ব্যবহার। OpenMP-র #pragma omp barrier আর GPU-র block-স্তরের synchronization একই ধারণা।
৯ · AI-এর যুগে condition variable-এর কোড
Producer-consumer বা thread pool চাইলে AI মুহূর্তে কোড দেবে, আর হালকা টেস্টে সম্ভবত চলবেও। কিন্তু এই পর্বের প্রায় সব বাগই “বেশিরভাগ সময় চলে” ধরনের — if-এর বাগ ধরা পড়ে শুধু তৃতীয় একটি থ্রেড ঠিক সময়ে এলে, lost wakeup শুধু ঠিক জায়গায় preemption হলে। AI-এর লেখা কোডে যা বারবার দেখা যায়:
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
if (!cond) wait(...) | stolen বা spurious wakeup-এ খালি থেকে নেওয়া | প্রতিটি wait একটি while-এ, বা wait_for/predicate-ওয়ালা রূপে কি না দেখুন |
| শর্তের flag lock ছাড়া বদলানো (“এটি তো শুধু একটি bool”) | lost wakeup, প্রোগ্রাম মাঝে মাঝে ঝুলে যায় | প্রতিটি শর্ত-ভ্যারিয়েবল কোন mutex দিয়ে রক্ষিত, লিখে মিলিয়ে নিন |
একটি condition variable-এ দুই রকম শর্ত, কিন্তু signal/notify | ভুল পক্ষ জাগে, সঠিকজন ঘুমিয়ে থাকে — deadlock-এর মতো আটকে যাওয়া | হয় আলাদা condition variable, নয় broadcast/notifyAll |
Latch, shutdown বা barrier-এ signal | একজন জাগে, বাকিরা চিরকাল ঘুমায় | “একটি পরিবর্তনে কতজন এগোতে পারে?” — একাধিক হলে broadcast |
Hoare semantics ধরে নিয়ে পাঠ্যবইয়ের if-ওয়ালা monitor সরাসরি Java বা pthreads-এ অনুবাদ | Mesa-তে ভুল | যে ভাষায় লিখছেন তার semantics জেনে নিন — প্রায় সবখানেই Mesa |
Java-য় synchronized-এর বাইরে wait(), বা Python-এ lock ছাড়া notify() | রানটাইমে exception | কোড চালিয়ে দেখুন; ডকুমেন্টেশনের শর্ত পড়ুন |
অপেক্ষার বদলে sleep(1) দিয়ে বারবার দেখা | ধীর, তবু race থাকে | sleep নয়, condition variable বা channel |
প্রম্পটে পরিবেশ, semantics আর যাচাইয়ের দাবি স্পষ্ট লিখুন:
AI-এর লেখা অপেক্ষার কোড ব্যবহারের আগে — চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Condition variable আর semaphore-এর মূল পার্থক্য? — semaphore একটি গণনা রাখে, তাই V মনে থাকে; condition variable-এর স্মৃতি নেই, অপেক্ষাকারী না থাকলে signal হারায় — শর্তটি থাকে mutex-রক্ষিত ভ্যারিয়েবলে।
- wait কেন mutex নেয়? — শর্ত দেখা আর ঘুমানোর মাঝে ফাঁক বন্ধ করতে; wait অবিভাজ্যভাবে mutex ছেড়ে ঘুমায়, তাই lost wakeup হয় না।
- Hoare আর Mesa semantics-এর পার্থক্য? — Hoare-এ signal পেলে জাগা থ্রেড সঙ্গে সঙ্গে চলে আর শর্ত নিশ্চিত সত্য (if চলে); Mesa-য় signal-দাতা চলতে থাকে, জাগা থ্রেড পরে চলে, তাই while বাধ্যতামূলক।
- Monitor কী? — shared ডেটা, তার procedure আর condition variable-এর একটি মডিউল, যেখানে একবারে একটিই থ্রেড সক্রিয় — mutual exclusion compiler বা ভাষা নিজে দেয়।
- Java-য় notify না notifyAll? — একটি অবজেক্টে wait set একটিই, তাই ভিন্ন শর্তের থ্রেড একসাথে থাকলে notifyAll নিরাপদ; একাধিক Condition লাগলে
ReentrantLock.
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Monitor-এর ধারণা কার? Brinch Hansen আর Hoare-এর কাজ, আর Concurrent Pascal কী প্রমাণ করেছিল?
১৯৬০-এর দশকের শেষে THE বা RC 4000-এর মতো multiprogramming সিস্টেম বানাতে গিয়ে সবাই দেখছিলেন, semaphore শক্তিশালী কিন্তু বিপজ্জনক — একটি ভুল P বা V পুরো সিস্টেমকে আটকে দেয়, আর কোড দেখে ভুল খোঁজা প্রায় অসম্ভব। RC 4000-এর nucleus-এর নকশাকার Per Brinch Hansen আর Tony Hoare-এর মধ্যে ১৯৭১–৭৩ সালের দিকে আলোচনা আর লেখার আদান-প্রদান চলে। Brinch Hansen ১৯৭২ সালে conditional critical region নিয়ে লেখেন, আর ১৯৭৩ সালে তাঁর বই Operating System Principles-এ “shared class” নামে monitor-এর ধারণা দেন: ডেটা আর তার ওপরের অপারেশনগুলো এক জায়গায়, পারস্পরিক বর্জন ভাষার নিয়মে।
Hoare ১৯৭৪ সালে Communications of the ACM-এ “Monitors: An Operating System Structuring Concept” প্রকাশ করেন। এতে ছিল “monitor” নাম, condition variable-এর সুনির্দিষ্ট “signal-and-wait” semantics, semaphore দিয়ে monitor বানানোর নির্মাণ (যা অংশ ৭-এ দেখেছেন), আর bounded buffer, alarm clock, disk head scheduler, readers–writers-এর মতো উদাহরণ — এবং সেই সাথে monitor-এর ভেতরের যুক্তির জন্য একটি প্রমাণ-পদ্ধতি (invariant আর condition-এর শর্ত)। Hoare প্রবন্ধে Brinch Hansen-এর অবদান স্বীকার করেন; তাই দুজনকেই monitor-এর জনক বলা হয়।
ধারণা কাগজে সুন্দর, কিন্তু কাজ করে কি? উত্তর দেন Brinch Hansen নিজে। ১৯৭৫ সালে তিনি Concurrent Pascal তৈরি করেন — Pascal-এর সাথে process, monitor আর class. এর বড় বৈশিষ্ট্য ছিল compile-এর সময়েই যাচাই: shared ডেটা শুধু monitor-এর ভেতর দিয়েই ছোঁয়া যায়, তাই একটি বড় শ্রেণির race ভাষার নিয়মেই অসম্ভব। এই ভাষায় লেখা হয় Solo নামের একটি একক-ব্যবহারকারীর OS আর কয়েকটি real-time সিস্টেম, যা দেখায় যে পুরো OS monitor দিয়ে কাঠামোবদ্ধ করা সম্ভব। পরে Modula, Mesa, Ada (protected object, Ada 95), আর শেষে Java আর C#-এ এই ধারণা নানা রূপে ঢোকে।
মূল উপলব্ধি: monitor নতুন কোনো ক্ষমতা আনেনি — semaphore দিয়েই এটি বানানো যায়। এর অবদান কাঠামো: সমন্বয়ের সব কোড ডেটার পাশে এক জায়গায়, আর সম্ভব হলে ভাষার নিয়মে যাচাইযোগ্য।
প্র ০২Xerox PARC-এর Mesa কেন Hoare-এর semantics ছেড়ে “signal-and-continue” বেছে নিল?
১৯৭০-এর দশকের শেষে Xerox PARC-এ Mesa ভাষা আর তা দিয়ে লেখা Pilot অপারেটিং সিস্টেম তৈরি হচ্ছিল — একটি বাস্তব, বড়, multi-process সিস্টেম, পাঠ্যবইয়ের উদাহরণ নয়। Butler Lampson আর David Redell ১৯৮০ সালের ফেব্রুয়ারিতে Communications of the ACM-এ “Experience with Processes and Monitors in Mesa” প্রবন্ধে লেখেন, Hoare-এর নকশা বাস্তবে কোথায় হোঁচট খায়, আর কী বদলাতে হলো।
প্রথম সমস্যা খরচ ও জটিলতা: Hoare-এ প্রতিটি signal-এ সঙ্গে সঙ্গে দুটি context switch — signal-দাতা থামে, জাগা জন চলে, পরে আবার উল্টো। দ্বিতীয়ত, Hoare-এর নিশ্চয়তা রাখতে scheduler-কে কঠোরভাবে বাধ্য থাকতে হয় — জাগা থ্রেডকে ঠিক তখনই চালাতে হবে, অগ্রাধিকার যা-ই হোক; priority-ভিত্তিক scheduling-এর সাথে এটি খাপ খায় না। তৃতীয়ত, বাস্তব সিস্টেমে আরও কিছু লাগে: অপেক্ষার timeout (ডিভাইস সাড়া না দিলে), কোনো process-কে বাইরে থেকে থামানো (abort), আর broadcast — যেখানে “একজন এখনই চলবে” ধারণাটিই অর্থহীন। তাই Mesa-তে NOTIFY হলো একটি ইঙ্গিত: জাগা process পরে কোনো এক সময় চলবে, আর তাকে WHILE লুপে শর্ত আবার দেখতে হবে। সঙ্গে এল BROADCAST আর timeout.
মূল্যটি ছোট — প্রতিটি wait-এর চারপাশে একটি লুপ, আর মাঝে মাঝে অকারণ জাগা। বিনিময়ে সঠিকতা আর signal-এর মধ্যে বাঁধন ছিঁড়ে যায়: অংশ ৩-এর যুক্তি অনুযায়ী while থাকলে বাড়তি signal কখনো ভুল করে না, তাই প্রোগ্রামার নিশ্চিত না হলে broadcast দিতে পারেন, আর বাস্তবায়নকারী spurious wakeup-এর অনুমতি নিয়ে দ্রুততর কোড লিখতে পারেন। প্রবন্ধটিতে nested monitor call (এক monitor-এর ভেতর থেকে আরেকটিতে wait করলে বাইরের lock ধরা থেকে যাওয়া), priority inversion-এর মতো সমস্যার আলোচনাও ছিল, যা পর্ব ২৯-এ আবার আসবে। pthreads, Java আর প্রায় সব আধুনিক সিস্টেম এই Mesa পথ ধরেছে।
মূল উপলব্ধি: তাত্ত্বিকভাবে সবচেয়ে শক্ত প্রতিশ্রুতি বাস্তবে সবচেয়ে দামি হতে পারে। দুর্বল প্রতিশ্রুতি (ইঙ্গিত) আর প্রতিটি অপেক্ষাকারীর নিজের পুনঃযাচাই মিলে একটি সহজ, দ্রুত আর বেশি সহনশীল ব্যবস্থা।
প্র ০৩Java-র “প্রতিটি অবজেক্ট একটি monitor” কি আসল monitor? Brinch Hansen কেন সমালোচনা করেছিলেন?
Java (১৯৯৫–৯৬) প্রথম জনপ্রিয় ভাষা, যেখানে থ্রেড আর monitor শুরু থেকেই ভাষার অংশ। প্রতিটি অবজেক্টের একটি intrinsic lock, synchronized method, আর wait/notify/notifyAll. দেখে মনে হয় Hoare-এর monitor সরাসরি লক্ষ লক্ষ প্রোগ্রামারের হাতে পৌঁছেছে। কিন্তু Per Brinch Hansen ১৯৯৯ সালে ACM SIGPLAN Notices-এ “Java's Insecure Parallelism” নামে কড়া সমালোচনা লেখেন।
তাঁর মূল আপত্তি: monitor-এর আসল শক্তি ছিল নিরাপত্তার নিশ্চয়তা — shared ডেটা শুধু monitor-এর procedure দিয়েই ছোঁয়া যাবে, আর compiler সেটি যাচাই করবে। Java-য় একটি class-এর কিছু method synchronized, কিছু নয় হতে পারে; field public হতে পারে; একই ডেটা lock ছাড়াই অন্য পথে ছোঁয়া যায়। Compiler এসব ধরে না। ফলে “monitor” শব্দটি থাকলেও যে নিশ্চয়তার জন্য monitor আবিষ্কার হয়েছিল, তা হারিয়ে গেছে — race আবার প্রোগ্রামারের সতর্কতার ওপর নির্ভর করে। তাঁর ভাষায় এটি ছিল ২৫ বছর আগের শিক্ষা ভুলে যাওয়া।
আরও দুটি ব্যবহারিক দুর্বলতা আছে। প্রতিটি অবজেক্টে condition একটিই (একটি wait set), তাই producer আর consumer একই সারিতে ঘুমায়, আর notify() ভুল জনকে জাগাতে পারে — এজন্য Java-র অনেক কোডে নিরাপত্তার খাতিরে notifyAll(), যা অনেক থ্রেডে ধীর। আর lock-টি অবজেক্টের সাথে প্রকাশ্য, তাই বাইরের কোডও synchronized (obj) লিখে আপনার lock নিয়ে বসে থাকতে পারে — এজন্য অভিজ্ঞরা একটি private lock অবজেক্ট রাখার পরামর্শ দেন। Java 5-এর java.util.concurrent এসব দুর্বলতার উত্তর: একাধিক Condition, timeout, আর BlockingQueue, CountDownLatch-এর মতো তৈরি হাতিয়ার, যাতে সাধারণ প্রোগ্রামারকে wait/notify ছুঁতেই না হয়। আজ Rust-এর মতো ভাষা Brinch Hansen-এর স্বপ্নের কাছাকাছি গেছে: Mutex<T>-এর ভেতরের ডেটা lock না নিয়ে ছোঁয়াই যায় না, compiler তা যাচাই করে।
মূল উপলব্ধি: একটি ধারণার নাম নেওয়া আর তার নিশ্চয়তা রাখা এক জিনিস নয়। monitor-এর মূল্য ছিল “ভুল করা অসম্ভব” — শুধু “ঠিক করা সম্ভব” নয়।
প্র ০৪Spurious wakeup কেন অনুমোদিত? মানদণ্ড কি বাস্তবায়নকারীদের সুবিধার জন্য প্রোগ্রামারদের ঝামেলায় ফেলল?
প্রথম দেখায় অদ্ভুত লাগে: একটি API কেন বলবে “আমি মাঝে মাঝে অকারণে ফিরব”? POSIX-এর rationale অংশে ব্যাখ্যাটি মোটামুটি এমন: multiprocessor-এ condition variable-কে কঠোরভাবে “ঠিক একজনকে, ঠিক signal-এর পরে” জাগানোর নিশ্চয়তা দিতে গেলে বাস্তবায়ন ধীর ও জটিল হয়ে যায়। কয়েকটি CPU একসাথে signal আর wait করলে কে আগে, কে পরে — তা নিখুঁতভাবে সাজাতে বাড়তি lock আর সমন্বয় লাগে। অন্যদিকে প্রোগ্রামারকে তো Mesa semantics-এর জন্য এমনিতেই while লুপ লিখতে হয়। লুপ যখন আছেই, তখন অকারণ জাগা সঠিকতায় কোনো ক্ষতি করে না — শুধু সামান্য সময় নেয়। তাই “অন্তত একজন” আর spurious wakeup-এর অনুমতি দিয়ে বাস্তবায়নকে দ্রুত হতে দেওয়া হয়েছে।
বাস্তবে কারণগুলো কী? Linux-এ একটি থ্রেড futex-এ ঘুমানোর সময় একটি signal handler চালাতে বাধা পেলে, ফিরে এসে আবার পুরো অবস্থা নিখুঁতভাবে মেলানোর চেয়ে wait থেকে ফিরে যাওয়া সহজ। একাধিক থ্রেডের signal আর broadcast-এর দৌড়ে দুজন জাগতে পারে। Windows-এর ডকুমেন্টেশন আর Java-র Object.wait-এর ডকুমেন্টেশনও একই সতর্কবার্তা দেয়। অন্যদিকে Go-র sync.Cond-এর ডকুমেন্টেশন বলে Wait শুধু Signal বা Broadcast-এ ফেরে — তবু লুপ রাখতে বলে, কারণ stolen wakeup সেখানেও আছে।
তাহলে কি প্রোগ্রামারদের ঝামেলা বাড়ল? আসলে উল্টো একটি সুবিধা আছে: spurious wakeup-এর সম্ভাবনা থাকায় কেউ “এই একবার if দিলেই চলবে” ভাবতে পারেন না — while লেখার অভ্যাস মানদণ্ডই বাধ্য করে, আর সেই অভ্যাস stolen wakeup, ভুল broadcast, timeout — সবকিছু থেকে একসাথে রক্ষা করে। এটি “defensive” নকশার একটি ভালো উদাহরণ: দুর্বল প্রতিশ্রুতি, কিন্তু এমন একটি ব্যবহার-রীতি যা সব অবস্থায় সঠিক। C++-এর condition_variable::wait(lock, pred), Python-এর wait_for বা Linux kernel-এর wait_event লুপটিকে API-র ভেতরে ঢুকিয়ে এই রীতিকে আরও সহজ করেছে।
মূল উপলব্ধি: একটি API-র প্রতিশ্রুতি যত দুর্বল, তার বাস্তবায়ন তত দ্রুত ও নমনীয় হতে পারে — যদি ব্যবহারের রীতি (while লুপ) সেই দুর্বলতা পুরোপুরি ঢেকে দেয়।
প্র ০৫Broadcast-এর “thundering herd” কী? signal বনাম broadcast-এর সিদ্ধান্ত বড় সিস্টেমে কীভাবে নেওয়া হয়?
ধরুন ১০০টি worker একটি condition variable-এ ঘুমিয়ে, আর একটি কাজ এল। broadcast দিলে ১০০ জনই জাগে, mutex-এর জন্য লাইন দেয়, একজন কাজটি পায়, ৯৯ জন শর্ত মিথ্যা দেখে আবার ঘুমায়। প্রতিবার ১০০টি context switch, mutex-এ ভিড়, cache-এ টানাটানি — শুধু একজনের কাজের জন্য। একে বলে thundering herd, ঈদের আগে বাসের টিকিট কাউন্টার খোলার ঘোষণায় সবাই একসাথে দৌড়ানোর মতো, অথচ টিকিট একটিই।
OS-এর অনেক জায়গায় এই সমস্যা দেখা গেছে। পুরোনো Unix সার্ভারে অনেক প্রসেস একই listening socket-এ accept()-এর জন্য অপেক্ষা করত, আর একটি সংযোগ এলে kernel সবাইকে জাগাত; Linux পরে accept-এ শুধু একজনকে জাগানোর ব্যবস্থা করে। epoll-এর ক্ষেত্রেও একই সমস্যা ছিল, তাই Linux 4.5-এ EPOLLEXCLUSIVE flag আসে, যাতে একটি ঘটনায় সব অপেক্ষাকারী না জাগে। Kernel-এর wait queue-তেও “exclusive” অপেক্ষাকারীর ধারণা আছে — wake_up তাদের মধ্যে একজনকেই জাগায়।
তাহলে নিয়ম কী? সঠিকতার প্রশ্নে while থাকলে broadcast সবসময় নিরাপদ, signal শুধু তখনই নিরাপদ, যখন (১) সব অপেক্ষাকারী একই শর্তে অপেক্ষা করছে, (২) একটি পরিবর্তনে ঠিক একজনই এগোতে পারে, আর (৩) যে জাগবে সে এগোতে না পারলে পরের জনকে জাগানোর দায় নেবে বা এমন অবস্থা আসবেই না। এগুলো না মিললে signal দিলে “অগ্রগতি হারানো” — সঠিকজন ঘুমিয়ে থাকে। তাই প্রকৌশলীরা সাধারণত (ক) প্রতিটি শর্তের জন্য আলাদা condition variable রাখেন, যাতে signal নিরাপদ হয়; (খ) shutdown বা অবস্থার বড় পরিবর্তনে broadcast দেন; (গ) মেপে দেখেন — profiler-এ mutex-এর প্রতিযোগিতা বা অকারণ জাগা বেশি দেখা গেলে তবেই নকশা বদলান। অনেক কাজ আবার একটি তৈরি blocking queue বা channel দিয়ে করা ভালো, যার ভেতরে কেউ এসব ইতিমধ্যে সামলেছেন।
মূল উপলব্ধি: broadcast সঠিকতার দিক থেকে নিরাপদ পছন্দ, signal কার্যক্ষমতার দিক থেকে। আগে সঠিক, তারপর মাপা, তারপর দ্রুত — আর আলাদা শর্তের জন্য আলাদা condition variable প্রায়ই দুটিকে একসাথে দেয়।
অনুশীলন
আউটপুট অনুমান, হাতে ট্রেস, interleaving খোঁজা আর কোড লেখা — আগে নিজে চেষ্টা করুন, তারপর উত্তর মেলান।
-
আউটপুট অনুমান: দুটি সংস্করণ। দুটিতেই main আগে
pthread_createদিয়ে থ্রেড T চালু করে, তারপর lock ধরে signal দেয়, তারপর T-কে join করে আর “main done” ছাপে। প্রতিটির সম্ভাব্য আউটপুট কী?/* version A -- T: */ lock(&m); while (!ready) wait(&cv, &m); unlock(&m); puts("T done"); /* version A -- main: */ lock(&m); ready = 1; signal(&cv); unlock(&m); /* version B -- T: */ lock(&m); wait(&cv, &m); unlock(&m); puts("T done"); /* version B -- main: */ lock(&m); signal(&cv); unlock(&m);A: সবসময়
T done, তারপরmain done. T আগে lock পেলে ready = 0 দেখে ঘুমায়, main-এর signal তাকে জাগায়। main আগে lock পেলে ready = 1 করে signal দেয় (কেউ শুনল না — ক্ষতি নেই), পরে T এসে ready = 1 দেখে ঘুমায়ই না। দুই ক্রমেই ঠিক — কারণ “স্মৃতি” আছেready-তে।B: দুটি সম্ভাবনা। T আগে wait-এ পৌঁছালে একই আউটপুট। কিন্তু main আগে lock পেলে (thread তৈরির পরপরই এটি খুবই সম্ভব) signal হারায়, T পরে wait করে চিরকাল ঘুমায়, আর main
pthread_join-এ আটকে থাকে — কিছুই ছাপা হয় না, প্রোগ্রাম ঝুলে যায়। এটি lost wakeup: শর্ত-ভ্যারিয়েবল ছাড়া condition variable অর্থহীন। (কদাচিৎ spurious wakeup-এ T জেগে “T done” ছাপতেও পারে — যা আরও বিভ্রান্তিকর।) -
হাতে ট্রেস (semaphore-এর monitor): অংশ ৭-এর নির্মাণে (Hoare semantics, semaphore-এর সারি FIFO) শুরুতে mutex = 1, next = 0, next_count = 0, x_sem = 0, x_count = 0. ঘটনা: A একটি procedure-এ ঢুকে
x.wait()করে; তারপর B একইভাবে ঢুকেx.wait()করে; তারপর C ঢুকেx.signal()করে। (ক) C-এরx.signal()শেষ হয়ে C ঘুমানোর মুহূর্তে পাঁচটি মান কত, আর ভেতরে কে? (খ) A procedure শেষ করার পর, আর C-ও বেরিয়ে যাওয়ার পর মানগুলো কত?A:
wait(mutex)1 → 0;x_count= 1; next_count = 0, তাইsignal(mutex)0 → 1;wait(x_sem)-এ ঘুম। B: একই — mutex 1 → 0 → 1,x_count= 2, x_sem-এ ঘুম (A-র পেছনে)। C: mutex 1 → 0;x_count= 2 > 0, তাই next_count = 1,signal(x_sem)সারির প্রথম জন A-কে জাগায়,wait(next)-এ C ঘুমায়।(ক) mutex = 0, next = 0, next_count = 1, x_sem = 0, x_count = 2 (A এখনো
x_count--চালায়নি); ভেতরে A; ঘুমিয়ে B (x_sem) আর C (next).(খ) A:
x_count= 1; শেষে next_count = 1 > 0, তাইsignal(next)— C জাগে। C: next_count = 0; শেষেsignal(mutex)0 → 1. চূড়ান্ত: mutex = 1, next = 0, next_count = 0, x_sem = 0, x_count = 1 — B এখনো x-এ অপেক্ষায়, যা ঠিক, কারণ signal হয়েছে একবারই। -
Interleaving খুঁজুন: অংশ ৭-এর
ResourceAllocatorহুবহু (if (busy) x.wait(time)) একটি Mesa-semantics ভাষায় অনুবাদ করা হলো। এমন একটি ঘটনাক্রম লিখুন, যাতে দুটি থ্রেড একসাথে সম্পদটি ব্যবহার করে। তারপর ঠিক করুন।- A
acquire: busy = false, তাই busy = true. A সম্পদ ব্যবহার শুরু করে। - B
acquire: busy = true, তাইx.wait-এ ঘুম। - A
release: busy = false,x.signal()— Mesa-য় B শুধু দরজার লাইনে যায়, A চলতে থাকে আর বেরিয়ে যায়। - B-এর আগে নতুন থ্রেড C লাইনে ছিল বা আগে lock পেল: busy = false দেখে busy = true, C ব্যবহার শুরু করে।
- B ভেতরে আসে;
ifআর দেখে না, সোজা busy = true — B-ও ব্যবহার শুরু করে। এখন B আর C দুজনেই সম্পদে — mutual exclusion ভাঙল।
সমাধান:
while (busy) x.wait(time);. তবে মনে রাখুন, এতে নিরাপত্তা ফেরে, কিন্তু “ছোট time আগে পাবে” প্রতিশ্রুতি দুর্বল হয় — C ঘুমায়ইনি, তবু আগে পেয়েছে। অগ্রাধিকার কঠোরভাবে রাখতে release-এর সময় সম্পদ সরাসরি B-এর নামে বরাদ্দ করুন (যেমনowner = B), আর acquire-এ শর্ত হোক “owner আমি কি না”। - A
-
কোড লিখুন: শুধু একটি pthread mutex আর একটি condition variable দিয়ে একটি counting semaphore বানান:
my_sem_wait(মান 0 হলে অপেক্ষা, তারপর ১ কমানো) আরmy_sem_post(১ বাড়ানো, একজন অপেক্ষাকারীকে জাগানো)। এখানে signal কেন যথেষ্ট, broadcast কেন লাগে না?typedef struct { pthread_mutex_t m; pthread_cond_t cv; int value; } my_sem; void my_sem_wait(my_sem *s) { pthread_mutex_lock(&s->m); while (s->value == 0) /* while, never if */ pthread_cond_wait(&s->cv, &s->m); s->value--; pthread_mutex_unlock(&s->m); } void my_sem_post(my_sem *s) { pthread_mutex_lock(&s->m); s->value++; /* the "memory" a condvar lacks */ pthread_cond_signal(&s->cv); pthread_mutex_unlock(&s->m); }signal যথেষ্ট, কারণ (১) সব অপেক্ষাকারী একই শর্তে (
value > 0) অপেক্ষা করে, আর (২) প্রতিটি post মান ঠিক ১ বাড়ায়, ফলে ঠিক একজনই এগোতে পারে। কেউ “চুরি” করলেও (না ঘুমিয়ে আসা একজন আগে নিলে) মান আবার 0, জাগা জন while-এ আবার ঘুমায় — আর যে নিয়েছে সে-ই সেই post-এর সুবিধাভোগী, তাই অগ্রগতি হারায় না। এখানেvalue-ই semaphore-এর স্মৃতি: কেউ অপেক্ষা না করার সময় post হলে value = 1 থেকে যায়, তাই পরের wait ঘুমায় না। এই নির্মাণ দেখায় যে condition variable + mutex দিয়ে semaphore, আর অংশ ৭ অনুযায়ী semaphore দিয়ে monitor — দুই দিকেই বানানো যায়; তাদের প্রকাশক্ষমতা সমান।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি consumer থ্রেড condition variable দিয়ে item-এর অপেক্ষা করে। ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Consumer mutex নেয় আর দেখে queue খালি
pthread_cond_waitএক ধাপে consumer-কে সারিতে রাখে আর mutex ছাড়ে- Producer mutex নিয়ে item রাখে আর signal দেয়
- Producer mutex ছাড়ে, আর consumer wait-এর ভেতরে mutex আবার পায়
- Consumer-এর while লুপ শর্ত আবার দেখে — item আছে
- Consumer item নিয়ে mutex ছাড়ে
POSIX-এ pthread_cond_wait(&cv, &m) ডাকলে কী ঘটে?
- mutex ধরে রেখেই থ্রেড ঘুমায়, যাতে কেউ শর্ত বদলাতে না পারে
- অবিভাজ্যভাবে m ছেড়ে cv-তে ঘুমায়, আর ফেরার আগে m আবার নেয়
- m ছেড়ে ঘুমায়, আর ফেরার সময় m ছাড়া অবস্থায় ফেরে
- শর্তটি সত্য না হওয়া পর্যন্ত wait নিজেই বারবার শর্ত দেখে
pthreads-এ condition variable-এর wait if-এর বদলে while লুপে রাখার কারণ কোনটি?
- while লুপ wait-কে দ্রুততর করে
- if দিলে compiler warning দেয়
- pthreads Hoare semantics মানে, তাই জাগার পর শর্ত নিশ্চিত সত্য
- spurious wakeup হতে পারে, আর জাগা থ্রেড চলার আগে অন্য থ্রেড শর্ত বদলে দিতে পারে
কোনো থ্রেড অপেক্ষা না করার সময় একটি condition variable-এ signal আর একটি semaphore-এ V দেওয়া হলো। পরে একটি থ্রেড condition variable-এ wait (শর্ত না দেখে) আর আরেকটি semaphore-এ P করল। কী ঘটে?
- condition variable-এর wait ঘুমায়, semaphore-এর P না ঘুমিয়ে এগোয়
- দুটিই না ঘুমিয়ে এগোয়
- দুটিই ঘুমায়
- condition variable-এর wait এগোয়, semaphore-এর P ঘুমায়
Hoare semantics-এর monitor-এ থ্রেড P x.signal() ডাকল, আর x-এ থ্রেড Q অপেক্ষা করছিল। ঠিক পরে কী হয়?
- P চলতে থাকে, Q দরজার লাইনে গিয়ে পরে ঢোকে
- P আর Q দুজনেই একসাথে monitor-এ চলে
- Q সঙ্গে সঙ্গে চলে, P একটি urgent queue-তে থেমে থাকে আর পরে entry queue-র আগে ফেরে
- P monitor ছেড়ে entry queue-র শেষে দাঁড়ায়
একটি countdown latch-এ ৫টি থ্রেড while (count > 0) wait(&cv, &m)-এ অপেক্ষা করছে। শেষ count_down-এ count 0 হলো, আর কোড pthread_cond_signal ডাকল (broadcast নয়)। সবচেয়ে সম্ভাব্য ফল?
- পাঁচজনই জেগে এগিয়ে যায়
- সাধারণত একজন এগোয়, বাকিরা চিরকাল ঘুমিয়ে থাকে
- একজন জেগে পরের জনকে জাগায়, এভাবে সবাই এগোয়
- pthread_cond_signal একটি error ফেরত দেয়
Semaphore দিয়ে Hoare-semantics monitor বানানোর পাঠ্যবইয়ের নির্মাণে (mutex, next, next_count, x_sem, x_count) next semaphore-এর কাজ কী?
- monitor-এর দরজা — নতুন আসা থ্রেডরা এখানে অপেক্ষা করে
- condition x-এর অপেক্ষাকারীরা এখানে ঘুমায়
- monitor-এর ভেতরে কতজন আছে তা গোনে
- signal দিয়ে থেমে যাওয়া থ্রেডদের পার্ক করে রাখে — Hoare-এর urgent queue
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- mutex + শর্ত + condition variable — তিনটি একসাথে। wait অবিভাজ্যভাবে mutex ছেড়ে ঘুমায়, ফেরার আগে আবার নেয়; signal একজনকে, broadcast সবাইকে জাগায়।
- wait সবসময় while-এ — spurious আর stolen wakeup থেকে রক্ষা; while থাকলে বাড়তি signal কখনো ভুল ঘটায় না।
- শর্ত দেখা আর বদলানো একই mutex ধরে — নইলে lost wakeup; condition variable-এর স্মৃতি নেই, স্মৃতি থাকে শর্ত-ভ্যারিয়েবলে।
- Monitor = ভেতরে বাঁধা mutual exclusion + condition. Hoare: signal-and-wait, শর্ত নিশ্চিত, বেশি switch. Mesa: signal-and-continue, ইঙ্গিত, while বাধ্যতামূলক — আজকের প্রায় সব সিস্টেম।
- Semaphore দিয়ে monitor (mutex, next, next_count, x_sem, x_count — baton passing), আর mutex + condvar দিয়ে semaphore — প্রকাশক্ষমতা সমান। একসাথে শুরু করাতে latch/barrier, যেখানে broadcast চাই।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৬ · ক্লাসিক সমস্যা — producer-consumer, readers-writers আর dining philosophersপরবর্তী পর্বcondition variable আর semaphore দিয়ে তিনটি ক্লাসিক সমস্যার পূর্ণ সমাধান।
- পর্ব ১৫ · POSIX thread হাতে-কলমে — তৈরি, join আর thread poolআগের ভিত্তিmutex আর condition variable দিয়ে বানানো ছোট thread pool — এই পর্বের ধারণার প্রথম ব্যবহার।
- অপারেটিং সিস্টেম · কোর্স হোম৬৪টি পর্বপুরো কোর্সের রোডম্যাপ — deadlock আর Banker's algorithm সামনে।
- Java · Thread আর synchronizationপাশের ট্র্যাকsynchronized, wait/notify আর java.util.concurrent — Java-র দিক থেকে।
- Python · Concurrencyপাশের ট্র্যাকthreading.Condition, queue.Queue আর GIL.
- DBMS · Concurrency controlএকই সমস্যা, অন্য জগৎডেটাবেসে lock-এর অপেক্ষা আর জাগানো — transaction-এর ভাষায়।