এই পর্বে যা শিখবেন
- System model — resource type, instance আর request → use → release; দুটি mutex দিয়ে বাস্তব deadlock বানিয়ে ধরা
- Deadlock-এর চারটি প্রয়োজনীয় শর্ত, কেন চারটিই লাগে তার প্রমাণ, আর কখন এগুলো যথেষ্ট
- Resource-allocation graph আঁকা, চক্র খোঁজা আর সিদ্ধান্ত নেওয়া — চক্র থাকলেও deadlock না হওয়ার উদাহরণসহ
- Deadlock সামলানোর চার পথ, প্রতিটি শর্ত ভাঙার কৌশল ও তার দাম, আর lock ordering কেন কাজ করে তার প্রমাণ
- Livelock, starvation আর deadlock-এর পার্থক্য, আর n(k − 1) + 1 ধরনের numerical সমস্যার পূর্ণ সমাধান
১ · ঢাকার মোড়ে জট — deadlock আসলে কী
অফিস ছুটির সময় ঢাকার কোনো চার রাস্তার মোড়ের কথা ভাবুন। সিগন্যাল বন্ধ, ট্রাফিক পুলিশও নেই। উত্তর দিক থেকে একটি বাস মোড়ের মাঝখানে ঢুকে পড়েছে, কিন্তু সামনে পূর্ব দিকের একটি প্রাইভেট কার দাঁড়িয়ে। সেই কার এগোতে পারছে না, কারণ তার সামনে দক্ষিণ দিকের একটি সিএনজি। সিএনজির সামনে পশ্চিম দিকের একটি ট্রাক, আর ট্রাকের সামনে — সেই প্রথম বাসটিই।
চারটি গাড়ির প্রত্যেকে রাস্তার এক টুকরো দখল করে পরের টুকরোর অপেক্ষায়। কেউ পিছু হটছে না, কেউ কাউকে তুলে সরাতেও পারছে না — বাইরের কেউ এসে একটি গাড়ি পিছিয়ে না দিলে এই gridlock কোনোদিন খুলবে না।
কম্পিউটারে ঠিক এই অবস্থার নাম deadlock. গাড়ির জায়গায় থ্রেড বা প্রসেস, রাস্তার টুকরোর জায়গায় lock, ফাইল, প্রিন্টার বা মেমরির মতো সম্পদ। সংজ্ঞাটি নিখুঁতভাবে বলা যায় এভাবে:
একদল প্রসেস deadlocked, যদি দলের প্রত্যেকে এমন একটি ঘটনার (সাধারণত কোনো সম্পদ ছেড়ে দেওয়ার) অপেক্ষায় থাকে, যা কেবল দলেরই অন্য কোনো প্রসেস ঘটাতে পারে। যেহেতু সবাই অপেক্ষায়, সেই ঘটনা কোনোদিন ঘটে না।
সবচেয়ে ছোট উদাহরণ লাগে মাত্র দুটি থ্রেড আর দুটি mutex. ধরুন একটি ব্যাংক সফটওয়্যারে দুটি হিসাব A আর B, প্রতিটির নিজের lock. থ্রেড T1 টাকা পাঠায় A থেকে B-তে, তাই আগে A-র lock নেয়, তারপর B-র। একই মুহূর্তে থ্রেড T2 টাকা পাঠায় B থেকে A-তে, তাই আগে B, তারপর A:
/* T1: transfer(A -> B) */ /* T2: transfer(B -> A) */
pthread_mutex_lock(&A); pthread_mutex_lock(&B);
pthread_mutex_lock(&B); /* waits */ pthread_mutex_lock(&A); /* waits */
move_money(); move_money();
pthread_mutex_unlock(&B); pthread_mutex_unlock(&A);
pthread_mutex_unlock(&A); pthread_mutex_unlock(&B);বেশিরভাগ সময় এই কোড ঠিকঠাক চলে। কিন্তু scheduler যদি কোনোদিন ঠিক এই ক্রমে চালায় — T1 নিল A, তারপর T2 নিল B — তাহলে T1 B-র জন্য আর T2 A-র জন্য চিরকাল অপেক্ষা করবে। প্রোগ্রাম crash করে না, error-ও দেখায় না; শুধু থেমে থাকে, CPU ব্যবহার শূন্য। test-এ হাজারবার চলে, আর ভারী load-এ রাত তিনটায় সার্ভার আটকে যায় — এটাই deadlock-এর কুখ্যাত স্বভাব।
১৯৬০-এর দশকের মাঝামাঝি Edsger Dijkstra এই সমস্যাকে নাম দেন “deadly embrace” — মরণ-আলিঙ্গন। তিনিই প্রথম একটি এড়ানোর পদ্ধতি দেন, যা পরে Banker's algorithm নামে পরিচিত হয় (পর্ব ২৮)। ১৯৬৮ সালে IBM-এর J. W. Havender OS/360-এর অভিজ্ঞতা থেকে দেখান, সব সম্পদ একসাথে চাওয়া বা একটি নির্দিষ্ট ক্রমে চাওয়া deadlock ঠেকায়। আর ১৯৭১ সালে E. G. Coffman, M. J. Elphick ও A. Shoshani ACM Computing Surveys-এ “System Deadlocks” প্রবন্ধে চারটি প্রয়োজনীয় শর্ত গুছিয়ে লেখেন, যা আজও প্রতিটি পাঠ্যবইয়ের ভিত্তি। পরের বছর R. C. Holt সম্পদ আর প্রসেসকে graph হিসেবে আঁকার পদ্ধতি জনপ্রিয় করেন।
২ · System model — resource type, instance আর request → use → release
Deadlock নিয়ে নিখুঁতভাবে ভাবতে হলে আগে একটি সরল মডেল লাগে। পাঠ্যবইগুলো সিস্টেমকে এভাবে দেখে:
- Resource type: একই রকম সম্পদের একটি শ্রেণি — R1, R2, … Rm. যেমন “প্রিন্টার”, “CPU core”, “হিসাব A-র lock”, “ডেটাবেসের একটি সারি”।
- Instance: প্রতিটি type-এর কয়টি একক আছে। একটি mutex-এর instance সবসময় একটি; একটি অফিসে একই মডেলের ৩টি প্রিন্টার থাকলে “প্রিন্টার” type-এর ৩টি instance — যেকোনো একটি পেলেই কাজ চলে।
- ব্যবহারের তিন ধাপ: প্রতিটি প্রসেস একটি সম্পদ ব্যবহার করে এই ক্রমে — request (চাওয়া; না পেলে অপেক্ষা), use (ব্যবহার), release (ছেড়ে দেওয়া)। Request আর release হয় system call বা library ফাংশন দিয়ে:
open/close,malloc/free,pthread_mutex_lock/unlock, semaphore-এরwait/signal.
| সম্পদ | Instance | Request | Release |
|---|---|---|---|
| একটি mutex | ১ | pthread_mutex_lock | pthread_mutex_unlock |
| Counting semaphore (মান ৫) | ৫ | sem_wait | sem_post |
| ৩টি একই প্রিন্টার | ৩ | প্রিন্ট-কাজ শুরু | কাজ শেষ |
| ডেটাবেসের একটি সারি | ১ (X lock) | SELECT … FOR UPDATE | COMMIT বা ROLLBACK |
| মেমরির পৃষ্ঠা | বহু | allocation | free |
একটি সূক্ষ্ম পার্থক্যও মনে রাখুন। Reusable resource (lock, প্রিন্টার, মেমরি) ব্যবহারের পর ফেরত আসে। Consumable resource (একটি বার্তা, একটি signal, pipe-এ লেখা ডেটা) একবার পেলে খরচ হয়ে যায়। দুই প্রসেস একে অপরের বার্তার অপেক্ষায় read-এ আটকে থাকলে সেটিও deadlock, যদিও কোনো lock নেই। তবে বাস্তবের বেশিরভাগ deadlock lock-এর, তাই এই পর্বের মূল আলোচনা reusable সম্পদ নিয়ে।
দুটি mutex দিয়ে আসল deadlock — আর তা ধরা
এবার উপরের বিপজ্জনক ক্রমটি ইচ্ছা করে ঘটাই। সমস্যা হলো, সত্যিকারের deadlock-এ প্রোগ্রাম চিরকাল আটকে থাকবে — অনলাইন রানারে চালানো যাবে না। তাই দুটি কৌশল: (১) একটি barrier দিয়ে দুই থ্রেডকে বাধ্য করি যাতে দুজনেই প্রথম lock নিয়ে তারপর দ্বিতীয়টি চায় — ফলে খারাপ interleaving প্রতিবারই ঘটে; (২) সাধারণ pthread_mutex_lock-এর বদলে timed lock pthread_mutex_timedlock, যা নির্দিষ্ট সময়ের বেশি অপেক্ষা করে না:
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <time.h>
static pthread_mutex_t A = PTHREAD_MUTEX_INITIALIZER, B = PTHREAD_MUTEX_INITIALIZER;
static pthread_barrier_t both_hold, both_tried;
struct worker { const char *name, *has, *wants; pthread_mutex_t *first, *second; int rc; };
static void *run(void *p) {
struct worker *w = p;
pthread_mutex_lock(w->first); /* hold one lock ... */
pthread_barrier_wait(&both_hold); /* force the bad interleaving */
struct timespec dl; clock_gettime(CLOCK_REALTIME, &dl);
dl.tv_nsec += 300000000L; /* ... and wait at most 0.3 s */
if (dl.tv_nsec >= 1000000000L) { dl.tv_sec++; dl.tv_nsec -= 1000000000L; }
w->rc = pthread_mutex_timedlock(w->second, &dl);
pthread_barrier_wait(&both_tried); /* nobody lets go before both tried */
if (w->rc == 0) pthread_mutex_unlock(w->second);
pthread_mutex_unlock(w->first); /* recovery: release and back off */
return NULL;
}
int main(void) {
struct worker t[2] = {{"T1", "A", "B", &A, &B, 0}, {"T2", "B", "A", &B, &A, 0}};
pthread_t id[2];
pthread_barrier_init(&both_hold, NULL, 2); pthread_barrier_init(&both_tried, NULL, 2);
for (int i = 0; i < 2; i++) pthread_create(&id[i], NULL, run, &t[i]);
for (int i = 0; i < 2; i++) pthread_join(id[i], NULL);
for (int i = 0; i < 2; i++)
printf("%s holds %s, waits for %s: %s\n", t[i].name, t[i].has, t[i].wants,
t[i].rc == ETIMEDOUT ? "timed out" : "got it");
if (t[0].rc == ETIMEDOUT && t[1].rc == ETIMEDOUT)
printf("deadlock detected: T1 -> B -> T2 -> A -> T1\n");
return 0;
}একটি আসল রানের আউটপুট:
T1 holds A, waits for B: timed out
T2 holds B, waits for A: timed out
deadlock detected: T1 -> B -> T2 -> A -> T1
প্রথম barrier পার হওয়ার মুহূর্তে T1-এর হাতে A আর T2-এর হাতে B — barrier ছাড়া এটা কালেভদ্রে ঘটত, এখন প্রতিবার ঘটে। তারপর দুজনেই অন্যজনের lock চায়, আর ঠিক ০.৩ সেকেন্ড পরে দুজনেরই pthread_mutex_timedlock ফেরে ETIMEDOUT নিয়ে। দ্বিতীয় barrier নিশ্চিত করে, দুজনের চেষ্টা শেষ না হওয়া পর্যন্ত কেউ lock ছাড়ে না — নইলে একজনের timeout-এর পর অন্যজন lock পেয়ে যেতে পারত। ছাপা হয় শুধু main থেকে, join-এর পরে, তাই আউটপুট প্রতিবার একই। সাধারণ pthread_mutex_lock হলে এই প্রোগ্রাম কোনোদিন শেষ হতো না। খেয়াল করুন, timeout আসলে একটি অনুমান: “এত সময় অপেক্ষা মানে সম্ভবত deadlock” — ধীর কিন্তু সুস্থ একটি থ্রেডকেও এটি ভুল করে deadlock ভাবতে পারে। pthread_mutex_timedlock আর pthread_barrier_t Linux-এর glibc-তে আছে, macOS-এ নেই।
৩ · চারটি শর্ত — কেন চারটিই লাগে
Coffman ও তাঁর সহকর্মীরা দেখান, deadlock ঘটতে হলে নিচের চারটি শর্ত একই সময়ে সত্য হতে হবে। এগুলোকে একসাথে বলে Coffman conditions. ঢাকার মোড়ের সাথে মিলিয়ে দেখুন:
| শর্ত | মানে | মোড়ের জটে | দুই mutex-এ |
|---|---|---|---|
| Mutual exclusion | অন্তত একটি সম্পদ ভাগ করা যায় না — একবারে একজনই ব্যবহার করে | রাস্তার এক টুকরোয় একটিই গাড়ি | mutex একবারে একটি থ্রেডের |
| Hold and wait | একটি সম্পদ ধরে রেখে আরেকটির জন্য অপেক্ষা | গাড়ি নিজের জায়গা না ছেড়েই সামনের জায়গার অপেক্ষায় | A ধরে রেখে B-র অপেক্ষা |
| No preemption | সম্পদ জোর করে কেড়ে নেওয়া যায় না; ধারক নিজে ছাড়লেই কেবল মুক্ত হয় | কোনো গাড়িকে ক্রেন দিয়ে তুলে সরানো যায় না | অন্য থ্রেড কারও mutex খুলে দিতে পারে না |
| Circular wait | P0 অপেক্ষা করছে P1-এর জন্য, P1 P2-এর জন্য … Pn আবার P0-এর জন্য | বাস → কার → সিএনজি → ট্রাক → বাস | T1 → T2 → T1 |
প্রমাণ: deadlock থাকলে চারটিই সত্য
পরীক্ষায় প্রায়ই জিজ্ঞেস করা হয়, “শর্তগুলো প্রয়োজনীয় (necessary) কেন?” যুক্তিটি ছোট, কিন্তু সুন্দর। ধরুন D একটি deadlocked প্রসেসের দল। সংজ্ঞা অনুযায়ী D-র প্রতিটি প্রসেস এমন একটি সম্পদের অপেক্ষায়, যা D-রই আরেকটি প্রসেস ধরে আছে।
- Circular wait: D-র প্রতিটি প্রসেস থেকে একটি তীর আঁকুন সেই প্রসেসের দিকে, যার জন্য সে অপেক্ষা করছে। প্রতিটি গিঁট থেকে অন্তত একটি তীর বেরোয়, আর গিঁটের সংখ্যা সসীম। তাই তীর ধরে হাঁটতে থাকলে একসময় আগে দেখা কোনো গিঁটে ফিরতেই হবে (pigeonhole নীতি) — অর্থাৎ একটি চক্র আছে।
- Hold and wait: চক্রের প্রতিটি প্রসেস অপেক্ষা করছে (তার থেকে তীর বেরিয়েছে), আবার কিছু ধরেও আছে (তার দিকে আগের প্রসেসের তীর এসেছে, অর্থাৎ আগেরজন তার হাতের সম্পদ চাইছে)।
- No preemption: সম্পদ কেড়ে নেওয়া গেলে OS চক্রের যেকোনো একটি সম্পদ কেড়ে অপেক্ষারতকে দিত, আর অপেক্ষা চিরকাল চলত না।
- Mutual exclusion: সম্পদ ভাগ করা গেলে চাওয়ামাত্র পাওয়া যেত — কাউকে অপেক্ষাই করতে হতো না।
অতএব deadlock ⇒ চারটি শর্ত। উল্টো দিক থেকে পড়লে এটিই deadlock prevention-এর মূল সূত্র: যেকোনো একটি শর্তকে কখনো সত্য হতে না দিলে deadlock অসম্ভব।
প্রয়োজনীয়, কিন্তু সবসময় যথেষ্ট নয়। চারটি শর্ত একসাথে সত্য হলেই কি deadlock নিশ্চিত? প্রতিটি resource type-এর একটিই instance হলে — হ্যাঁ। তখন চক্রের প্রতিটি প্রসেস যে সম্পদ চাইছে, সেটির একমাত্র কপি চক্রেরই পরের প্রসেসের হাতে, আর সে-ও আটকে। কিন্তু একাধিক instance থাকলে চক্রটি “মিথ্যা অ্যালার্ম” হতে পারে: চক্রের বাইরের কোনো প্রসেস একই type-এর আরেকটি instance ছেড়ে দিলেই চক্র ভেঙে যায়। অংশ ৫-এ ঠিক এমন একটি উদাহরণ ধাপে ধাপে দেখবেন।
৪ · Resource-allocation graph — ছবিতে কে কী ধরে আছে
চারটি শর্ত মুখে বলা সহজ, কিন্তু দশটি প্রসেস আর পাঁচ রকম সম্পদ হলে মাথায় রাখা কঠিন। তাই আঁকা হয় resource-allocation graph (সংক্ষেপে RAG) — একটি directed graph, যার নিয়ম চারটি:
- বৃত্ত = একটি প্রসেস (P1, P2 …)।
- আয়তক্ষেত্র = একটি resource type; ভেতরের প্রতিটি বিন্দু = একটি instance.
- Request edge Pi → Rj: Pi Rj-এর একটি instance চেয়েছে, এখনো পায়নি, অপেক্ষা করছে। তীর যায় আয়তক্ষেত্রের কিনারা পর্যন্ত — কারণ যেকোনো একটি instance হলেই চলবে।
- Assignment edge Rj → Pi: Rj-এর একটি নির্দিষ্ট instance Pi-এর হাতে। তীর শুরু হয় নির্দিষ্ট বিন্দু থেকে।
RAG-এর চিহ্ন
Process = circle, resource type = rectangle, instance = dot
এবার একটি পরিচিত পাঠ্যবই-উদাহরণ ধাপে ধাপে গড়ে তুলি। তিনটি প্রসেস; চারটি resource type — R1 (১টি instance), R2 (২টি), R3 (১টি), R4 (৩টি)। প্রতিটি ধাপে একটি করে ঘটনা ঘটবে, আর ডান পাশের টেবিলে দেখবেন কে অপেক্ষায়, চক্র আছে কি না।
| অপেক্ষায় | |
| মুক্ত instance | |
| চক্র | |
| Deadlock? |
- শুরু: শুধু গিঁট। তিনটি প্রসেস (সবুজ বৃত্ত) আর চারটি resource type (হলুদ আয়তক্ষেত্র)। বিন্দুর সংখ্যাই instance-এর সংখ্যা: R1-এ ১, R2-তে ২, R3-এ ১, R4-এ ৩. এখনো কেউ কিছু ধরেনি, কেউ কিছু চায়নি।
- চারটি বরাদ্দ (নীল তীর)। R1-এর একমাত্র instance P2-এর হাতে; R2-এর দুটি instance-এর একটি P1-এর, অন্যটি P2-এর; R3 P3-এর হাতে। প্রতিটি তীর শুরু হয়েছে নির্দিষ্ট বিন্দু থেকে। R4-এর তিনটিই মুক্ত। কেউ অপেক্ষায় নেই, তাই চক্রের প্রশ্নই নেই।
- P1 চাইল R1 (কমলা ভাঙা তীর)। R1-এর একমাত্র instance P2-এর কাছে, তাই P1 অপেক্ষায়। graph-এ পথ তৈরি হলো P1 → R1 → P2: “P1 অপেক্ষা করছে P2-এর জন্য।” এখনো চক্র নয় — P2 অপেক্ষায় নেই।
- এবার P2 চাইল R3. R3 আছে P3-এর হাতে, তাই P2-ও অপেক্ষায়। পথ লম্বা হলো: P1 → R1 → P2 → R3 → P3. শিকল তৈরি হয়েছে, কিন্তু শেষ মাথায় P3 কিছুর অপেক্ষায় নেই।
- চক্র নেই — তাই deadlock নেই। P3-এর কোনো request edge নেই, অর্থাৎ সে চলতে পারে। কাজ শেষে R3 ছাড়লে P2 সেটি পাবে, P2 শেষ করে R1 আর R2 ছাড়বে, তখন P1 R1 পাবে। শিকলটি একটি একটি করে খুলে যাবে। এটাই প্রথম উপপাদ্য: চক্র না থাকলে deadlock নেই।
- P3 চাইল R2 — চক্র তৈরি। R2-এর দুটি instance-ই P1 আর P2-এর হাতে, আর দুজনেই অপেক্ষায়। লাল দাগে প্রথম চক্র: P1 → R1 → P2 → R3 → P3 → R2 → P1. এবার কোনো প্রসেসই এগোতে পারে না। R4-এর তিনটি মুক্ত instance কোনো কাজে আসে না, কারণ কেউ R4 চায়নি।
- দ্বিতীয় চক্র, আর রায়। একই graph-এ আরেকটি চক্র: P2 → R3 → P3 → R2 → P2. R2-এর দুটি instance থাকায় শুধু চক্র দেখে রায় দেওয়া যায় না; কিন্তু হিসাব করে দেখুন — R2-এর দুই ধারক P1 আর P2 নিজেরাই অপেক্ষায়, চক্রের বাইরে কেউ নেই যে কিছু ছাড়বে। তাই P1, P2, P3 তিনজনই deadlocked.
তিনটি উপপাদ্য আর তাদের যুক্তি
১ · চক্র নেই ⇒ deadlock নেই
চক্রহীন directed graph-এ এমন অন্তত একটি প্রসেস আছে যার কোনো request edge নেই (তীর ধরে হাঁটলে একসময় থামতেই হয়)। সে শেষ করে সম্পদ ছাড়ে, graph ছোট হয়, আবার চক্রহীন থাকে — আরোহ (induction) দিয়ে সবাই শেষ করে।
যেকোনো instance-সংখ্যায় সত্য২ · চক্র + সব single instance ⇒ deadlock
চক্রের প্রতিটি প্রসেস যে সম্পদের অপেক্ষায়, তার একমাত্র instance চক্রের পরের প্রসেসের হাতে, যে নিজেও অপেক্ষায়। কেউ ছাড়বে না — সংজ্ঞা অনুযায়ী deadlock.
চক্র ⇔ deadlock৩ · চক্র + multiple instance ⇒ হয়তো
চক্র তখন শুধু প্রয়োজনীয় শর্ত। চক্রের বাইরের কেউ একই type-এর instance ধরে থাকলে এবং সে শেষ করতে পারলে, চক্র ভেঙে যায়। নিশ্চিত হতে লাগে detection algorithm.
পর্ব ২৮-এ হিসাব৫ · চক্র মানেই deadlock নয় — আর wait-for graph
তৃতীয় উপপাদ্যের পাঠ্যবই-উদাহরণটি পরীক্ষায় সবচেয়ে বেশি আসে। দুটি resource type, R1 আর R2, প্রত্যেকটির ২টি instance; চারটি প্রসেস। চক্র আছে, তবু কোনো deadlock নেই। কেন, তা ধাপে ধাপে দেখুন:
| অপেক্ষায় | |
| চক্র | |
| মুক্ত instance | |
| চলছে / শেষ |
- অবস্থা। R1-এর দুটি instance P2 আর P3-এর হাতে; R2-এর দুটি P1 আর P4-এর হাতে। P1 চাইছে R1, P3 চাইছে R2 — দুজনেই অপেক্ষায়, কারণ কোনো type-এর মুক্ত instance নেই।
- চক্রটি দেখুন। লাল দাগে P1 → R1 → P3 → R2 → P1. single-instance হলে এটাই deadlock-এর প্রমাণ হতো। কিন্তু এখানে R1-এর আরেক ধারক P2, আর R2-এর আরেক ধারক P4 — দুজনেই চক্রের বাইরে।
- চক্রের বাইরের দুজন মুক্ত। P2 আর P4-এর কোনো request edge নেই, অর্থাৎ তারা কিছুর অপেক্ষায় নেই — চলতে পারে, শেষ করতে পারে। এই “পালানোর পথ”-ই চক্রটিকে ক্ষতিহীন করেছে।
- P4 শেষ করল। P4 তার R2 instance ছাড়ল, আর OS সেটি অপেক্ষারত P3-কে দিল (সবুজ তীর)। P3-এর request edge উল্টে assignment edge হলো — চক্র ভেঙে গেল।
- P3 চলছে। P3-এর হাতে এখন R1 আর R2 দুটোরই একটি করে instance. তার আর কিছু চাওয়ার নেই, তাই সে কাজ শেষ করবে। P1 এখনো R1-এর অপেক্ষায়, কিন্তু সেই অপেক্ষা এখন সসীম।
- সবাই শেষ করে। P3 শেষে R1 আর R2 ছাড়ল; R1-এর সেই instance P1 পেল (সবুজ)। P2-ও এর মধ্যে শেষ করে তার R1 ফিরিয়ে দিয়েছে। শেষে P1-ও শেষ করবে। পুরো সময় চক্র থাকা সত্ত্বেও কেউ চিরকাল আটকে ছিল না।
Multiple instance-এর RAG দেখে সিদ্ধান্ত নিতে “reduction” করুন: যে প্রসেসের সব অনুরোধ এখনকার মুক্ত instance দিয়ে মেটানো যায় (বা যার কোনো অনুরোধই নেই), তাকে শেষ হতে দিন, তার সব সম্পদ মুক্ত করুন, আবার খুঁজুন। সবাইকে এভাবে সরানো গেলে deadlock নেই; কয়েকজন আটকে থাকলে ঠিক তারাই deadlocked. উপরের প্রথম graph-এ (অংশ ৪-এর ধাপ ৭) শুরুতেই কাউকে সরানো যায় না, আর এই graph-এ P2, P4, P3, P1 ক্রমে সবাই সরে যায়। পর্ব ২৮-এ এই পদ্ধতিই matrix দিয়ে নিখুঁত algorithm হবে।
যন্ত্র দিয়ে যাচাই — একটি ছোট RAG cycle detector
নিচের simulator একটি তীরের তালিকা নেয় ("P1>R1" মানে request, "R1>P2" মানে assignment), DFS দিয়ে প্রতিটি সরল চক্র খোঁজে, আর বলে deadlock নিশ্চিত কি না। চারটি graph-এ চালানো হয়েছে: দুই mutex-এর উল্টো ক্রম, আর উপরের তিনটি পাঠ্যবই-graph:
// RAG: "P1>R1" = request edge (P1 waits for R1), "R1>P2" = assignment edge (an R1 unit is held by P2)
function analyse(name, units, edges) {
const adj = {};
for (const e of edges) { const [a, b] = e.split(">"); (adj[a] = adj[a] || []).push(b); }
const cycles = [];
function dfs(start, v, path) { // every simple cycle, listed once: from its smallest node
for (const w of adj[v] || []) {
if (w === start) cycles.push(path);
else if (w > start && !path.includes(w)) dfs(start, w, [...path, w]);
}
}
for (const s of Object.keys(adj).sort()) dfs(s, s, [s]);
console.log(name);
if (cycles.length === 0) { console.log(" no cycle -> NO deadlock (certain)"); return; }
let certain = false;
for (const c of cycles) {
const multi = c.filter(n => n[0] === "R" && units[n] > 1);
if (multi.length === 0) certain = true;
console.log(" cycle: " + [...c, c[0]].join(" -> ") +
(multi.length ? " (multi-unit: " + multi.map(r => r + "x" + units[r]).join(", ") + ")" : " (all single-unit)"));
}
console.log(certain ? " => deadlock CERTAIN" : " => deadlock POSSIBLE only: run a detection algorithm");
}
analyse("G1 two mutexes, opposite order", { R1: 1, R2: 1 },
["R1>P1", "P1>R2", "R2>P2", "P2>R1"]);
analyse("G2 textbook graph, no cycle", { R1: 1, R2: 2, R3: 1, R4: 3 },
["P1>R1", "P2>R3", "R1>P2", "R2>P2", "R2>P1", "R3>P3"]);
analyse("G3 same graph + P3 requests R2", { R1: 1, R2: 2, R3: 1, R4: 3 },
["P1>R1", "P2>R3", "R1>P2", "R2>P2", "R2>P1", "R3>P3", "P3>R2"]);
analyse("G4 cycle with spare holders", { R1: 2, R2: 2 },
["P1>R1", "P3>R2", "R1>P2", "R1>P3", "R2>P1", "R2>P4"]);আউটপুট:
G1 two mutexes, opposite order
cycle: P1 -> R2 -> P2 -> R1 -> P1 (all single-unit)
=> deadlock CERTAIN
G2 textbook graph, no cycle
no cycle -> NO deadlock (certain)
G3 same graph + P3 requests R2
cycle: P1 -> R1 -> P2 -> R3 -> P3 -> R2 -> P1 (multi-unit: R2x2)
cycle: P2 -> R3 -> P3 -> R2 -> P2 (multi-unit: R2x2)
=> deadlock POSSIBLE only: run a detection algorithm
G4 cycle with spare holders
cycle: P1 -> R1 -> P3 -> R2 -> P1 (multi-unit: R1x2, R2x2)
=> deadlock POSSIBLE only: run a detection algorithm
G1-এ চক্রের দুটি সম্পদই single-unit, তাই রায় নিশ্চিত — এটাই অংশ ১-এর দুই mutex. G2-তে (অংশ ৪-এর ধাপ ৫) কোনো চক্র নেই, তাই কোনো সন্দেহ ছাড়াই deadlock নেই। G3 (ধাপ ৬–৭) আর G4 (অংশ ৫) দেখতে একই রকম — দুটিতেই চক্র, দুটিতেই multi-unit সম্পদ, দুটিতেই রায় “POSSIBLE”. অথচ হাতে reduction করে আগেই দেখেছেন G3 সত্যিই deadlocked আর G4 নয়। শিক্ষা: multiple instance-এ graph-এর আকার দেখে রায় হয় না, সংখ্যা গুনতে হয়। w > start শর্তের কারণে প্রতিটি চক্র তার সবচেয়ে ছোট নামের গিঁট থেকে একবারই আসে।
Wait-for graph — single instance হলে ছোট ছবি
সব resource single-instance হলে RAG থেকে resource-গিঁটগুলো মুছে আরও ছোট একটি graph বানানো যায়: Pi → R → Pj পথ থাকলে সরাসরি তীর Pi → Pj, মানে “Pi অপেক্ষা করছে Pj-এর জন্য।” এর নাম wait-for graph. এখানে deadlock আছে যদি এবং কেবল যদি চক্র থাকে, আর চক্র খোঁজা যায় DFS দিয়ে O(V + E) সময়ে (DSA কোর্সের DFS পর্ব)।
RAG (single instance) wait-for graph
P1 → R1 → P2 → R3 → P3 P1 ──→ P2 ──→ P3
P3 → R4 → P1 ↑ │
└─────────────┘ চক্র = deadlock
ডেটাবেস ঠিক এই কাজটিই করে — কোন transaction কার lock-এর অপেক্ষায়, তার wait-for graph রেখে নিয়মিত চক্র খোঁজে, আর একটিকে rollback করে জট খোলে। পুরো ব্যবস্থাটি, wait-die আর wound-wait সহ, DBMS কোর্সের concurrency control পর্বে বিস্তারিত আছে; এখানে আর পুনরাবৃত্তি করছি না।
৬ · Deadlock সামলানোর চার পথ — আর কেন বেশিরভাগ OS “উটপাখি”
সম্ভাব্য সব কৌশল চারটি দলে পড়ে:
| পদ্ধতি | মূল চিন্তা | দাম | কোথায় দেখা যায় |
|---|---|---|---|
| উপেক্ষা (ostrich) | ধরে নেওয়া deadlock ঘটে না; ঘটলে মানুষ restart করবে | বিরল ঘটনায় হ্যাং | Linux, Windows, macOS — অ্যাপের lock-এর ক্ষেত্রে |
| Prevention | নকশা দিয়েই চারটি শর্তের একটি অসম্ভব করা | কম সম্পদ-ব্যবহার, নিয়মের বোঝা | lock ordering, সব lock একসাথে নেওয়া |
| Avoidance | প্রতিটি অনুরোধে হিসাব করে দেখা, দিলে সিস্টেম “নিরাপদ” থাকে কি না | আগে থেকে সর্বোচ্চ চাহিদা জানতে হয় | Banker's algorithm (পর্ব ২৮), কিছু real-time সিস্টেম |
| Detection ও recovery | ঘটতে দেওয়া, নিয়মিত চক্র খোঁজা, পেলে একজনকে বলি দেওয়া | খোঁজার খরচ, rollback-এর ব্যবস্থা লাগে | ডেটাবেস, Java-র jstack-এর রিপোর্ট |
উটপাখির কৌশল কেন যুক্তিসংগত
Andrew Tanenbaum তাঁর পাঠ্যবইয়ে মজা করে একে বলেন ostrich algorithm — বিপদ দেখলে বালিতে মাথা গুঁজে রাখা। শুনতে দায়িত্বহীন, কিন্তু প্রকৌশলের হিসাবে প্রায়ই এটাই ঠিক:
- খরচ সব সময়ের, লাভ কালেভদ্রে। প্রতিটি
pthread_mutex_lock-এ চক্র খুঁজলে সবচেয়ে ঘন ঘন চলা অপারেশনটি ধীর হতো, অথচ বেশিরভাগ প্রোগ্রামে deadlock ঘটেই না। - OS সব “সম্পদ” চেনে না, আর ধরলেও সারাতে পারে না। user-space spinlock বা বার্তার অপেক্ষা kernel দেখতে পায় না; আর অর্ধেক লেখা ফাইল “ফিরিয়ে নেওয়ার” উপায় নেই (ভাবনার প্রশ্ন ১)।
তবু কিছু সহায়তা আছে: POSIX-এর PTHREAD_MUTEX_ERRORCHECK mutex একই থ্রেড দ্বিতীয়বার lock নিতে গেলে আটকে না গিয়ে EDEADLK ফেরায়; Linux-এ fcntl-এর record lock-এ (F_SETLKW) kernel সরল চক্র ধরে EDEADLK দেয়। আর kernel-এর নিজের lock-এ deadlock মানে পুরো মেশিন অচল, তাই সেখানে lockdep-এর মতো কঠোর যাচাই চলে (অংশ ৮)।
Avoidance আর detection — এক নজরে, বিস্তারিত পর্ব ২৮-এ। Avoidance-এ প্রতিটি প্রসেস আগেই ঘোষণা করে সে কোন সম্পদের সর্বোচ্চ কতটি নেবে। OS প্রতিটি অনুরোধের আগে দেখে, অনুরোধটি মঞ্জুর করলে সিস্টেম safe state-এ থাকে কি না — অর্থাৎ এমন একটি ক্রম আছে কি না, যে ক্রমে সবাই নিজের সর্বোচ্চ চাহিদা পেয়ে শেষ করতে পারে। না থাকলে প্রসেসটিকে অপেক্ষা করানো হয়, যদিও সম্পদ মুক্ত আছে। Dijkstra-র Banker's algorithm এই হিসাবটি করে। Detection-এ অনুরোধ সঙ্গে সঙ্গে মঞ্জুর হয়, আর মাঝে মাঝে উপরের reduction-এর মতো একটি algorithm চালিয়ে আটকে থাকা প্রসেস খোঁজা হয়; পেলে recovery — একটি “victim” প্রসেস বন্ধ করা, বা তার সম্পদ কেড়ে নিয়ে তাকে আগের কোনো checkpoint-এ ফিরিয়ে নেওয়া।
৭ · Prevention — চারটি শর্তের প্রতিটি ভাঙার কৌশল ও তার দাম
অংশ ৩-এর প্রমাণ বলছে, যেকোনো একটি শর্ত ভাঙলেই যথেষ্ট। প্রতিটির জন্য পরিচিত কৌশল আছে, কিন্তু কোনোটিই বিনা খরচে নয়।
কৌশল: সম্পদকে ভাগযোগ্য করা। Read-only ফাইল একসাথে হাজারজন পড়তে পারে — সেখানে কখনো deadlock নেই। প্রিন্টার নিজে ভাগ করা যায় না, কিন্তু spooling দিয়ে এমন ভান করা যায়: প্রতিটি প্রসেস তার প্রিন্ট-কাজ ডিস্কের একটি সারিতে লিখে চলে যায়, আর একটিমাত্র spooler প্রসেস প্রিন্টারের মালিক হয়ে একে একে ছাপে। কেউ প্রিন্টার “ধরে রেখে” অপেক্ষা করে না।
দাম ও সীমা: mutex-এর পুরো উদ্দেশ্যই একচেটিয়া হওয়া, তাই সব সম্পদ এভাবে বদলানো যায় না। Spooler-এর ডিস্কও একটি সম্পদ — দুটি বড় কাজ অর্ধেক লিখে ডিস্ক ভরালে সেখানেও জট। আধুনিক রূপ: lock-free ডেটা স্ট্রাকচার আর RCU (পর্ব ২৯)।
কৌশল ক: সব সম্পদ একসাথে চাওয়া। প্রসেস শুরুতেই যা যা লাগবে সব একবারে চায়; হয় সব পায়, নয় কিছুই পায় না। Windows-এ WaitForMultipleObjects (সব অপেক্ষা, bWaitAll = TRUE) একাধিক mutex একবারে পরমাণুভাবে নেয়; C++17-এর std::scoped_lock একাধিক mutex এমনভাবে নেয় যাতে deadlock না হয়।
কৌশল খ: কিছু চাওয়ার আগে হাতের সব ছেড়ে দেওয়া। pthread_mutex_trylock দিয়ে দ্বিতীয় lock চাইতে গিয়ে না পেলে প্রথমটিও ছেড়ে, একটু অপেক্ষা করে আবার চেষ্টা।
দাম: সম্পদ অকারণে আটকে থাকে (শেষ মিনিটে লাগবে এমন প্রিন্টারও শুরু থেকে দখলে), তাই resource utilization কমে। অনেক জনপ্রিয় সম্পদ চাওয়া প্রসেস “সবগুলো একসাথে মুক্ত” মুহূর্ত না পেয়ে starvation-এ পড়তে পারে। প্রায়ই আগে থেকে বোঝাই যায় না কী লাগবে। আর কৌশল খ livelock-এর দরজা খোলে (অংশ ৮)।
কৌশল: অপেক্ষায় পড়লে হাতের সম্পদ কেড়ে নেওয়া। কোনো প্রসেস একটি অনুরোধে আটকালে OS তার হাতের সম্পদগুলো কেড়ে নেয়, আর পরে সব একসাথে ফেরত পেলে তবেই সে চলে। অথবা অপেক্ষারত কেউ যে সম্পদ চায়, তা আরেক অপেক্ষারত প্রসেসের হাতে থাকলে সেখান থেকে কেড়ে আনা।
কোথায় কাজ করে: যে সম্পদের অবস্থা সহজে সংরক্ষণ করে পরে ফিরিয়ে আনা যায় — CPU (register সংরক্ষণ করে context switch, পর্ব ৯), মেমরির পৃষ্ঠা (ডিস্কে swap করে), ডেটাবেস transaction (rollback করে আবার চালানো)।
কোথায় কাজ করে না: mutex, প্রিন্টার, টেপ ড্রাইভ — অর্ধেক আপডেট করা ডেটার lock কেড়ে নিলে অন্যজন ভাঙা ডেটা দেখবে (ভাবনার প্রশ্ন ২)।
কৌশল: সব সম্পদের একটি global order. প্রতিটি resource type-কে একটি সংখ্যা দিন: F(R1) < F(R2) < … নিয়ম: একটি প্রসেস কেবল তখনই Rj চাইতে পারবে, যখন তার হাতের প্রতিটি সম্পদের সংখ্যা F(Rj)-এর চেয়ে ছোট। দুই হিসাবের ক্ষেত্রে: সবসময় আগে ছোট id-র হিসাব, তারপর বড়টি — টাকা যেদিকেই যাক।
দাম: শৃঙ্খলা — ক্রমটি লিখে রাখতে ও যাচাই করতে হয়, আর বড়-নম্বরের lock নেওয়ার পর ছোটটি লাগলে বড়টি ছেড়ে দুটিই ক্রমে নিতে হয়। তবু খরচ প্রায় শূন্য আর নিশ্চয়তা পূর্ণ বলে বাস্তবে এটিই সবচেয়ে প্রচলিত।
মোড়ের জটেও একই চার সমাধান। অনেক দেশে মোড়ে হলুদ ক্রস-দাগের “box junction” থাকে — সামনে বের হওয়ার জায়গা না থাকলে box-এ ঢোকা নিষেধ; এটি hold and wait ভাঙে। ফ্লাইওভার রাস্তাকে কার্যত ভাগযোগ্য করে, ট্রাফিক পুলিশের একটি গাড়িকে পিছু হটানো হলো preemption, আর সিগন্যালের পালা একটি ক্রম চাপিয়ে দেয়।
প্রমাণ: global lock order থাকলে circular wait অসম্ভব
নিচের ডায়াগ্রামে চারটি lock, নম্বর ১ থেকে ৪. তিনটি থ্রেড চেষ্টা করবে একটি চক্র বানাতে; দেখুন নিয়মটি কোথায় তাদের থামায়, আর কেন সেটি যেকোনো দৈর্ঘ্যের চক্রের জন্য সত্য।
| সবচেয়ে লম্বা অপেক্ষার শিকল | |
| শিকলে h-এর ধারা | |
| চক্র সম্ভব? |
- নিয়মটি। চারটি lock-কে স্থায়ী নম্বর (rank) দেওয়া হয়েছে: L1 = ১ … L4 = ৪. প্রতিটি থ্রেড কেবল সেই lock চাইতে পারে, যার নম্বর তার হাতের সব lock-এর নম্বরের চেয়ে বড়। h মানে একটি থ্রেডের হাতের সবচেয়ে বড় নম্বর।
- প্রথম অপেক্ষা। T1 ধরে আছে L1 (h = ১), T2 ধরে আছে L3, T3 ধরে আছে L4. T1 চায় L3: ৩ > ১, তাই অনুরোধ বৈধ; কিন্তু L3 T2-র কাছে, তাই T1 অপেক্ষায়। wait-for graph-এ প্রথম তীর T1 → T2.
- শিকল লম্বা হলো। T2 চায় L4: ৪ > ৩, বৈধ; L4 T3-র কাছে, তাই T2-ও অপেক্ষায়। শিকল T1 → T2 → T3, আর পথ ধরে h বাড়ছে: ১, ৩, ৪. তীর সবসময় “বড় নম্বরের ধারক”-এর দিকে যায়।
- চক্র বন্ধ করার চেষ্টা নিষিদ্ধ। চক্র বন্ধ করতে T3-কে চাইতে হবে L1. কিন্তু T3-এর হাতে L4, আর ১ > ৪ মিথ্যা — নিয়ম অনুরোধটিই করতে দেয় না। ক্রম-মানা কোড (যেমন পরের প্রোগ্রামের
lock_pair) এমন অনুরোধ লেখেই না। - সাধারণ প্রমাণ। ধরুন তবু কোনো চক্র T1 → … → Tn → T1 আছে। Ti যে lock চাইছে তার নম্বর wᵢ > hᵢ (নিয়ম); সেই lock Ti+1-এর হাতে, তাই hᵢ₊₁ ≥ wᵢ > hᵢ. চক্র ঘুরে এলে h₁ < h₁ — যা অসম্ভব। তাই কোনো দৈর্ঘ্যের চক্রই হতে পারে না, আর circular wait ছাড়া deadlock নেই।
- তাহলে T3 কী করে? তার L1 আর L4 দুটোই লাগলে সে আগে L1 চায় — হাতে কিছু না রেখেই অপেক্ষা করে। T2 L4 পেয়ে শেষ করে, তারপর T1 L3 পেয়ে শেষ করে L1 ছাড়ে, শেষে T3 L1 আর L4 পায়। অপেক্ষার প্রতিটি শিকল এমন কারও কাছে শেষ হয় যে নিজে অপেক্ষায় নেই, তাই সবাই এগোয়।
এবার একই দুই-হিসাবের সমস্যা, কিন্তু একটি lock_pair ফাংশন দিয়ে, যা টাকা যেদিকেই যাক সবসময় ছোট id-র হিসাব আগে lock করে। দুই থ্রেড উল্টো দিকে লাখো বার টাকা পাঠাবে:
#include <pthread.h>
#include <stdio.h>
struct account { int id; long balance; pthread_mutex_t m; };
static struct account acc[2] = {{.id = 0, .balance = 100000}, {.id = 1, .balance = 100000}};
static void lock_pair(struct account *x, struct account *y) {
if (x->id > y->id) { struct account *t = x; x = y; y = t; } /* smaller id first */
pthread_mutex_lock(&x->m);
pthread_mutex_lock(&y->m);
}
struct job { struct account *from, *to; int n; };
static void *transfer(void *p) {
struct job *j = p;
for (int i = 0; i < j->n; i++) {
lock_pair(j->from, j->to);
j->from->balance -= 1;
j->to->balance += 1;
pthread_mutex_unlock(&j->from->m);
pthread_mutex_unlock(&j->to->m);
}
return NULL;
}
int main(void) {
for (int i = 0; i < 2; i++) pthread_mutex_init(&acc[i].m, NULL);
struct job j1 = {&acc[0], &acc[1], 200000}, j2 = {&acc[1], &acc[0], 150000};
pthread_t t1, t2;
pthread_create(&t1, NULL, transfer, &j1); /* A -> B */
pthread_create(&t2, NULL, transfer, &j2); /* B -> A */
pthread_join(t1, NULL); pthread_join(t2, NULL);
printf("both threads finished: 200000 x (A->B), 150000 x (B->A)\n");
printf("A = %ld, B = %ld, A + B = %ld\n", acc[0].balance, acc[1].balance,
acc[0].balance + acc[1].balance);
return 0;
}আউটপুট:
both threads finished: 200000 x (A->B), 150000 x (B->A)
A = 50000, B = 150000, A + B = 200000
সাড়ে তিন লাখ লেনদেনে দুই থ্রেড বারবার একই দুটি lock-এর জন্য প্রতিযোগিতা করেছে, তবু প্রোগ্রাম শেষ হয়েছে — কারণ দুজনেই আগে acc[0] (A), তারপর acc[1] (B) নেয়, টাকা যেদিকেই যাক। কেউ B ধরে রেখে A-র অপেক্ষা করে না, তাই উল্টো দিকের তীর কখনো তৈরি হয় না। হিসাবও মেলে: A = ১,০০,০০০ − ২,০০,০০০ + ১,৫০,০০০ = ৫০,০০০, B = ১,৫০,০০০, মোট ২,০০,০০০ — শুরুতেও তাই ছিল, কারণ mutex-এর ভেতরে কোনো লেনদেন হারায়নি। unlock-এর ক্রম যেকোনো হতে পারে; ছাড়ার সময় কেউ অপেক্ষা করে না বলে তা deadlock-এর সাথে সম্পর্কহীন। একটি সূক্ষ্ম ফাঁদ: from আর to একই হিসাব হলে lock_pair একই mutex দুবার নেবে — অনুশীলন ৩-এ এটি ঠিক করবেন।
৮ · বাস্তবের কোডে — lock hierarchy, lockdep, livelock আর distributed deadlock
Lock hierarchy আর Linux-এর lockdep
বড় প্রজেক্টে প্রতিটি lock-কে নম্বর দেওয়ার বদলে স্তর বা lock hierarchy লিখে রাখা হয়: যেমন “আগে ফাইল-সিস্টেমের lock, তারপর inode-এর, তারপর page-এর” — উল্টো কখনো নয়। Linux kernel-এর বহু সোর্স ফাইলের শুরুতে এমন মন্তব্য থাকে, যেখানে কোন lock কার আগে নিতে হবে তা লেখা। দুটি একই রকম বস্তু একসাথে lock করতে হলে (যেমন scheduler-এর দুটি CPU-র run queue) kernel ঐতিহাসিকভাবে ঠিকানার ক্রম মেনেছে — ঠিক আমাদের lock_pair-এর মতো।
নিয়ম লিখে রাখলেই সবাই মানে না, তাই ২০০৬ সালে (kernel 2.6.18) Ingo Molnar ও Arjan van de Ven-এর তৈরি lockdep যুক্ত হয়। CONFIG_PROVE_LOCKING চালু করা debug kernel-এ এটি প্রতিটি lock নেওয়ার সময় হিসাব রাখে, “কোন lock ধরে থাকা অবস্থায় কোন lock নেওয়া হলো”। এই সম্পর্কগুলো দিয়ে সে একটি dependency graph বানায়, আর নতুন তীরে চক্র তৈরি হলে সতর্ক করে — deadlock আসলে না ঘটলেও। অর্থাৎ একদিন A → B আর অন্যদিন B → A ক্রম দেখলেই রিপোর্ট আসে, দুই থ্রেডের ভাগ্যক্রমে একসাথে আটকানোর দরকার পড়ে না:
======================================================
WARNING: possible circular locking dependency detected
------------------------------------------------------
kworker/0:1 is trying to acquire lock:
(&dev->lock_b){+.+.}, at: driver_work+0x40/0x90
but task is already holding lock:
(&dev->lock_a){+.+.}, at: driver_work+0x20/0x90
which lock already depends on the new lock.User-space-এ Valgrind-এর helgrind আর ThreadSanitizer lock-order-inversion রিপোর্ট করে (পর্ব ১৫)। Java-র jstack চক্র পেলে লেখে “Found one Java-level deadlock”. Go-র runtime সব goroutine একসাথে ঘুমিয়ে পড়লে থামিয়ে দেয় all goroutines are asleep - deadlock! বলে, তবে আংশিক deadlock ধরে না। আর Linux-এর khungtaskd কোনো task অনেকক্ষণ (সাধারণত ১২০ সেকেন্ড) uninterruptible ঘুমে থাকলে সতর্ক করে — প্রমাণ নয়, লক্ষণ।
আরও গভীরে: lockdep প্রতিটি lock instance আলাদা করে দেখে না, দেখে lock-এর class — সাধারণত যে কোড-লাইনে lock-টি initialize হয়েছে, সেটিই class. তাই হাজার inode-এর হাজার lock একটি class, আর একবার শেখা ক্রম সব instance-এ প্রযোজ্য; একই class-এর দুটি lock বৈধভাবে একসাথে নিতে হলে spin_lock_nested()-এর মতো ফাংশনে subclass বলে দিতে হয়। প্রতিটি নতুন dependency-তে সে graph-এ চক্র খোঁজে, আর একবার যাচাই করা জোড়া hash করে মনে রাখে, যাতে খরচ সীমিত থাকে। সে আরও যাচাই করে কোনো lock interrupt-এর ভেতরে নেওয়া হয় কিন্তু বাইরের কোডে interrupt চালু রেখে নেওয়া হয় কি না — কারণ সেখানে একটি থ্রেড একাই নিজের interrupt handler-এর সাথে deadlock করতে পারে। বিস্তারিত আছে kernel-এর Documentation/locking/lockdep-design.rst নথিতে।
Deadlock, livelock আর starvation — তিনটি আলাদা অসুখ
অনেকে hold-and-wait ভাঙতে trylock ব্যবহার করেন: দ্বিতীয় lock না পেলে প্রথমটিও ছেড়ে আবার চেষ্টা। দুই থ্রেড যদি একদম একই তালে এটি করে, তাহলে দুজনে একসাথে নেয়, একসাথে ব্যর্থ হয়, একসাথে ছাড়ে, একসাথে আবার নেয় … চিরকাল। কেউ আটকে নেই, CPU পুরো ব্যস্ত, কিন্তু কাজ শূন্য। এর নাম livelock — সরু গলিতে মুখোমুখি দুজন মানুষ, দুজনেই ভদ্রতা করে একই দিকে সরে যাচ্ছেন বারবার।
| Deadlock | Livelock | Starvation | |
|---|---|---|---|
| প্রসেসের অবস্থা | blocked, ঘুমিয়ে | চলছে, অবস্থা বদলাচ্ছে | প্রস্তুত বা অপেক্ষায়, কিন্তু পালা আসে না |
| CPU ব্যবহার | প্রায় শূন্য | বেশি | অন্যরা ব্যবহার করছে |
| কারা আক্রান্ত | চক্রের সবাই | সংশ্লিষ্ট সবাই | একজন বা কয়েকজন; বাকিরা দিব্যি চলে |
| নিজে থেকে কাটে? | কখনো না | ভাগ্য ভালো হলে, তাল ভাঙলে | load কমলে কাটতে পারে |
| প্রতিকার | ক্রম, prevention, detection | random backoff, ক্রম | aging, FIFO সারি, ন্যায্য lock |
বাস্তবের বিখ্যাত উদাহরণ receive livelock: ১৯৯০-এর দশকে Jeffrey Mogul ও K. K. Ramakrishnan দেখান, প্রতিটি নেটওয়ার্ক প্যাকেটে interrupt নেওয়া kernel প্রচণ্ড ভিড়ে শুধু interrupt সামলাতেই CPU শেষ করে, প্যাকেট প্রক্রিয়ার সময় পায় না। Dining philosophers-এ (পর্ব ২৬) সবাই একসাথে বাঁ কাঁটা নিলে deadlock, আর “না পেলে রেখে আবার চেষ্টা” করলে livelock — একই সমস্যার দুই রূপ।
Distributed system আর ডেটাবেসে deadlock — সংক্ষেপে
একাধিক মেশিনে ছড়ানো সিস্টেমে কারও কাছে পুরো wait-for graph নেই — প্রতিটি সার্ভার শুধু নিজের lock-গুলো জানে। T1 সার্ভার X-এ T2-র অপেক্ষায়, আর T2 সার্ভার Y-এ T1-এর অপেক্ষায়; দুই সার্ভারের নিজস্ব graph-এ চক্র নেই, অথচ মিলিয়ে দেখলে আছে। কেন্দ্রীয়ভাবে graph জোগাড় করলে দেরিতে আসা তথ্যে এমন চক্রও দেখা যায় যা কখনো ছিল না — একে বলে phantom deadlock. তাই বাস্তবে timeout আর timestamp-ভিত্তিক prevention-ই বেশি চলে (ভাবনার প্রশ্ন ৫)। ডেটাবেসের দিকটি DBMS কোর্সের concurrency control পর্বে, আর distributed সিস্টেমের OS-দিক পর্ব ৫৩-এ।
৯ · পরীক্ষার numerical — “কমপক্ষে কতটি resource লাগবে?”
বিশ্ববিদ্যালয়, চাকরি আর ইন্টারভিউর পরীক্ষায় সবচেয়ে বেশি আসা deadlock-numerical এটি: “একটি সিস্টেমে n টি প্রসেস, প্রত্যেকের একই ধরনের সম্পদের সর্বোচ্চ k টি একক লাগে। deadlock কখনো হবে না, তা নিশ্চিত করতে কমপক্ষে কতটি একক লাগবে?” উত্তর:
ভিন্ন ভিন্ন চাহিদা k1, k2 … হলে: Rmin = Σ(ki − 1) + 1
প্রমাণ (দুই দিক থেকে)
ধরে নেওয়া মডেল: প্রসেস এককগুলো একটি একটি করে চায়, যা পায় তা ধরে রাখে, আর নিজের সর্বোচ্চ ki টি পেলে কাজ শেষ করে সব ফেরত দেয়।
- R = Σ(ki − 1) হলে deadlock সম্ভব: প্রত্যেককে ki − 1 টি দিন। সব একক শেষ, অথচ কেউ নিজের শেষ এককটি পায়নি — সবাই অপেক্ষায়, কেউ কিছু ছাড়বে না। এর চেয়ে কম R-এ একইভাবে সবাইকে অসম্পূর্ণ রেখে সব একক শেষ করা যায়।
- R = Σ(ki − 1) + 1 হলে deadlock অসম্ভব: deadlock-এ সব একক বরাদ্দ হয়ে থাকতে হবে (একটি মুক্ত থাকলে কোনো অপেক্ষারত প্রসেস সেটি পেত), আর কেউ নিজের পূর্ণ ki পায়নি — অর্থাৎ প্রত্যেকের হাতে বড়জোর ki − 1. তাহলে মোট বরাদ্দ ≤ Σ(ki − 1) < R, অর্থাৎ অন্তত একটি একক মুক্ত — বিরোধ। pigeonhole নীতি অনুযায়ী কাউকে না কাউকে শেষ এককটি পেতেই হবে; সে শেষ করে সব ফেরত দেয়, আর বাকিদের জন্য একই যুক্তি আবার খাটে।
একই শর্তের আরেকটি পরিচিত রূপ: Σ ki < R + n হলে deadlock-মুক্ত। দুই দিক থেকে n বিয়োগ করলেই Σ(ki − 1) < R, অর্থাৎ R ≥ Σ(ki − 1) + 1 — একই কথা।
পাঁচটি সমাধান করা উদাহরণ
| প্রশ্ন | হিসাব | উত্তর |
|---|---|---|
| ৩টি প্রসেস, প্রত্যেকের ২টি টেপ ড্রাইভ লাগে। কমপক্ষে কতটি? | ৩ × (২ − ১) + ১ | ৪ (৩টি হলে সবাই ১টি করে নিয়ে আটকে যেতে পারে) |
| ৪টি প্রসেস, প্রত্যেকের সর্বোচ্চ ৩টি একক | ৪ × ২ + ১ | ৯ |
| ৩টি প্রসেস, প্রত্যেকের সর্বোচ্চ ৪টি | ৩ × ৩ + ১ | ১০ |
| চাহিদা ৩, ৪ আর ৬ — কমপক্ষে কতটি? | ২ + ৩ + ৫ + ১ | ১১ |
| উল্টো প্রশ্ন: ৬টি একক, প্রতিটি প্রসেসের ২টি লাগে। সর্বোচ্চ কতটি প্রসেস চললেও deadlock হবে না? | n(২ − ১) + ১ ≤ ৬ ⇒ n ≤ ৫ | ৫ (৬টি প্রসেস হলে প্রত্যেকে ১টি নিয়ে আটকাতে পারে) |
আরেকটি উল্টো রূপ: “১২টি একক আর ৪টি প্রসেস; প্রত্যেকের সর্বোচ্চ চাহিদা k হলে k-এর সবচেয়ে বড় মান কত, যাতে deadlock না হয়?” ৪(k − ১) + ১ ≤ ১২ ⇒ k − ১ ≤ ২.৭৫ ⇒ k ≤ ৩.৭৫, পূর্ণসংখ্যায় k = ৩. যাচাই: k = ৪ হলে প্রত্যেকে ৩টি করে নিলে ১২টিই শেষ, সবাই আটকে।
সূত্রে অন্ধ বিশ্বাস না করে যন্ত্র দিয়ে যাচাই করুন। নিচের simulator সূত্র ব্যবহার না করে প্রতিটি সম্ভাব্য “সবাই অসম্পূর্ণ, কিছুই মুক্ত নেই” অবস্থা খুঁজে দেখে, আর সবচেয়ে ছোট R বের করে যেখানে এমন অবস্থা নেই:
// Units are requested one at a time; a process that reaches its max need finishes and frees all.
// Deadlock = every unfinished process holds fewer than its need, and no unit is free.
function stuckState(need, R) { // search all hold-vectors h[i] in 0..need[i]-1
const h = new Array(need.length).fill(0);
function go(i, used) {
if (used > R) return null;
if (i === need.length) return used === R ? [...h] : null; // all R held, none free
for (let x = 0; x < need[i]; x++) { h[i] = x; const r = go(i + 1, used + x); if (r) return r; }
return null;
}
return go(0, 0);
}
function minUnits(need) {
let R = 1;
while (stuckState(need, R)) R++; // grow R until no stuck state exists
return R;
}
const cases = [[2, 2, 2], [3, 3, 3, 3], [4, 4, 4], [3, 4, 6]];
for (const need of cases) {
const R = minUnits(need), formula = need.reduce((s, k) => s + (k - 1), 0) + 1;
console.log("needs [" + need + "]: brute force R = " + R + ", formula sum(k-1)+1 = " + formula +
" | with R-1 = " + (R - 1) + " stuck at holds [" + stuckState(need, R - 1) + "]");
}
// reverse question: 6 units, every process needs 2 -> how many processes are always safe?
let n = 1;
while (!stuckState(new Array(n + 1).fill(2), 6)) n++;
console.log("6 units, each needs 2: at most " + n + " processes are deadlock-free");আউটপুট:
needs [2,2,2]: brute force R = 4, formula sum(k-1)+1 = 4 | with R-1 = 3 stuck at holds [1,1,1]
needs [3,3,3,3]: brute force R = 9, formula sum(k-1)+1 = 9 | with R-1 = 8 stuck at holds [2,2,2,2]
needs [4,4,4]: brute force R = 10, formula sum(k-1)+1 = 10 | with R-1 = 9 stuck at holds [3,3,3]
needs [3,4,6]: brute force R = 11, formula sum(k-1)+1 = 11 | with R-1 = 10 stuck at holds [2,3,5]
6 units, each needs 2: at most 5 processes are deadlock-free
প্রতিটি সারিতে brute force আর সূত্র একই উত্তর দিয়েছে: ৪, ৯, ১০ আর ১১ — উপরের টেবিলের সাথে হুবহু। শেষ অংশে R − 1 টি এককে আটকে যাওয়ার একটি উদাহরণ, ঠিক প্রমাণের প্রথম দিকের মতো: প্রত্যেকের হাতে ki − 1 টি, যেমন [2,3,5] হলে সবাই এক কম পেয়ে আটকে। শেষ লাইন উল্টো প্রশ্নের উত্তর মেলায়: ৬টি এককে, ২টি-চাওয়া প্রসেস ৫টি পর্যন্ত নিরাপদ। খোঁজা হয় সব hold-vector-এ, তাই প্রসেস বা চাহিদা বেশি হলে সময় দ্রুত বাড়ে — এটি যাচাইয়ের টুল, বড় সিস্টেম চালানোর নয়।
সূত্রটি কোথায় খাটে না: এটি একটিমাত্র resource type-এর জন্য, যেখানে সব একক সমান। একাধিক type হলে (যেমন প্রিন্টার আর স্ক্যানার দুটোই) প্রতিটির জন্য আলাদা করে সূত্র মেলানো যথেষ্ট নয় — type-গুলোর মধ্যে চক্র হতে পারে; তখন RAG বা Banker's-এর হিসাব লাগে। আর “কমপক্ষে n(k − 1) + 1” মানে এর চেয়ে কম হলে deadlock সম্ভব, অবশ্যম্ভাবী নয়।
“RAG এঁকে সিদ্ধান্ত নিন” — একটি পূর্ণ সমাধান
প্রশ্ন: R1 (১টি), R2 (২টি), R3 (১টি)। P1 ধরে আছে R2-এর একটি, চাইছে R1; P2 ধরে আছে R1, চাইছে R3; P3 ধরে আছে R3 আর R2-এর একটি, চাইছে R2. deadlock আছে কি?
সমাধান: তীরগুলো: R2 → P1, P1 → R1, R1 → P2, P2 → R3, R3 → P3, R2 → P3, P3 → R2. চক্র খুঁজুন: P1 → R1 → P2 → R3 → P3 → R2 → P1 — চক্র আছে। R2-তে দুটি instance, তাই reduction করুন: মুক্ত instance — R1: ০, R2: ০, R3: ০. কারও অনুরোধ মেটানো যায় না (P1-এর R1, P2-এর R3, P3-এর R2 — সবগুলোই ০)। কোনো প্রসেস সরানো যায় না, আর চক্রের বাইরে কোনো প্রসেস নেই। রায়: P1, P2, P3 deadlocked. এবার ধরুন একটি চতুর্থ প্রসেস P4 R2-এর একটি instance ধরে থাকত আর কিছু চাইত না (তখন R2-এর ৩টি instance) — P4 শেষ করে R2 ছাড়লে P3 পেত, আর জট খুলে যেত।
১০ · AI-এর যুগে deadlock-এর কোড
AI খুব দ্রুত থ্রেডের কোড লিখে দেয়, আর সেই কোড আপনার ল্যাপটপে প্রায় সবসময় চলে — কারণ deadlock-এর জন্য দরকার নির্দিষ্ট একটি interleaving, যা হালকা load-এ কালেভদ্রে ঘটে। “আমার মেশিনে চলে” তাই কোনো প্রমাণ নয়। AI-এর যে ভুলগুলো এই বিষয়ে বারবার দেখা যায়:
| AI-এর ভুল | কেন বিপজ্জনক | কীভাবে ধরবেন |
|---|---|---|
| দুই ফাংশনে একই দুটি lock উল্টো ক্রমে | কালেভদ্রে চিরস্থায়ী হ্যাং | প্রতিটি lock-এর জন্য “কার পরে কে” তালিকা বানান; ThreadSanitizer/helgrind চালান |
| “deadlock এড়াতে” trylock-এর লুপ, কোনো random backoff ছাড়া | livelock আর CPU পুড়ে যাওয়া | অনেক থ্রেড দিয়ে চালিয়ে CPU আর throughput মাপুন |
| numerical-এ n × k বা n(k − 1) উত্তর | এক বেশি বা কম — পরীক্ষায় ভুল | ছোট উদাহরণে হাতে “সবাইকে k − 1 দিন” পরীক্ষা |
| “চক্র আছে, তাই deadlock” — multi-instance graph-এ | ভুল রায় | reduction করে দেখুন কেউ শেষ করতে পারে কি না |
macOS-এ pthread_mutex_timedlock বা barrier, Windows-এ POSIX কোড | compile-ই হয় না | কোন OS-এর জন্য, তা প্রম্পটে বলুন; man page দেখুন |
AI-কে কোড পর্যালোচনা করাতে চাইলে ঢালাও “কোডটা ঠিক আছে?” না বলে নির্দিষ্ট বিশ্লেষণ চান, আর এমন পরীক্ষা চান যা আপনি নিজে চালিয়ে যাচাই করতে পারবেন:
AI-এর দেওয়া concurrent কোড হাতে পেলে — চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Deadlock-এর চারটি প্রয়োজনীয় শর্ত কী কী? — mutual exclusion, hold and wait, no preemption, circular wait; চারটি একসাথে সত্য না হলে deadlock হয় না।
- RAG-এ চক্র থাকলেই কি deadlock? — প্রতিটি resource-এর একটি instance হলে হ্যাঁ; একাধিক instance হলে না-ও হতে পারে, reduction বা detection algorithm লাগে।
- n টি প্রসেসের প্রত্যেকের k টি লাগলে কমপক্ষে কতটি resource? — n(k − 1) + 1; তার চেয়ে একটি কম হলে সবাই k − 1 টি নিয়ে আটকে যেতে পারে।
- দুই থ্রেড, দুই lock — deadlock কীভাবে ঠেকাবেন? — দুজনেই একই global ক্রমে lock নেবে (যেমন id বা ঠিকানার ক্রমে); এতে circular wait অসম্ভব।
- Livelock আর deadlock-এর পার্থক্য? — deadlock-এ সবাই ঘুমিয়ে অপেক্ষা করে; livelock-এ সবাই চলছে ও অবস্থা বদলাচ্ছে, কিন্তু কেউ অগ্রগতি করে না।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Linux বা Windows অ্যাপের deadlock ধরার চেষ্টাই করে না, অথচ PostgreSQL বা MySQL প্রতিনিয়ত ধরে। এই পার্থক্য কেন যুক্তিসংগত?
পার্থক্যটা আসে দুটি প্রশ্নের উত্তর থেকে: সম্পদগুলো কে চেনে, আর ধরা পড়লে সারানো যায় কি না।
ডেটাবেসের ক্ষেত্রে দুটোর উত্তরই “হ্যাঁ”। প্রতিটি সারি বা টেবিলের lock নেয় ডেটাবেসের নিজের lock manager, তাই কে কার অপেক্ষায় তা একটি টেবিলেই লেখা আছে — wait-for graph বানানো সহজ। আর ধরা পড়লে একটি transaction-কে বলি দেওয়া যায়, কারণ ডেটাবেস log রাখে: rollback করলে ওই transaction-এর সব পরিবর্তন মুছে যায়, ডেটা থাকে সুসংগত, আর অ্যাপ্লিকেশন ত্রুটি দেখে আবার চেষ্টা করে। PostgreSQL একটি lock-এর জন্য deadlock_timeout (সাধারণত ১ সেকেন্ড) অপেক্ষার পর চক্র খোঁজে; InnoDB প্রতিটি অপেক্ষার সাথেই খোঁজে।
সাধারণ OS-এর ক্ষেত্রে দুটোর উত্তরই প্রায় “না”। একটি অ্যাপ user-space-এ নিজের spinlock বানাতে পারে, shared memory-তে একটি flag ঘুরিয়ে অপেক্ষা করতে পারে, বা অন্য প্রসেসের socket-বার্তার অপেক্ষায় থাকতে পারে — kernel এর অনেককিছুই “সম্পদ” হিসেবে দেখতেই পায় না। futex-ভিত্তিক mutex-এ uncontended অবস্থায় kernel-এ ঢোকাই হয় না। আর ধরা পড়লেও OS কী করবে? একটি প্রসেস বন্ধ করলে তার অর্ধেক লেখা ফাইল, পাঠানো বার্তা, অর্ধেক করা লেনদেন থেকে যায় — কোনো সাধারণ “undo” নেই।
খরচের হিসাবও আছে। lock নেওয়া একটি প্রোগ্রামের সবচেয়ে ঘন ঘন অপারেশনগুলোর একটি; প্রতিবার চক্র খুঁজলে সবার গতি কমত, অথচ বেশিরভাগ প্রোগ্রামে deadlock কখনো ঘটে না। Tanenbaum-এর ভাষায়, বিরল আর কম ক্ষতির বিপদের জন্য বিশাল খরচ করা বুদ্ধিমানের কাজ নয় — উটপাখি তখন যুক্তিবাদী।
তবে OS পুরোপুরি চোখ বন্ধ রাখে না: PTHREAD_MUTEX_ERRORCHECK নিজের সাথে deadlock ধরে, Linux-এর fcntl record lock-এ সরল চক্র ধরে EDEADLK দেয়, kernel-এর নিজের lock-এর জন্য debug build-এ lockdep আছে, আর hung-task detector দীর্ঘ আটকে থাকা task-এর খবর দেয়।
মূল উপলব্ধি: detection তখনই অর্থবহ, যখন সিস্টেম সব সম্পদ চেনে আর একটি কাজকে নিরাপদে ফিরিয়ে নিতে পারে। যেখানে এ দুটি নেই, সেখানে দায়িত্ব নকশার — lock ordering দিয়ে deadlock-কে শুরু থেকেই অসম্ভব করা।
প্র ০২CPU আর মেমরি তো OS অহরহ কেড়ে নেয় — তাহলে mutex কেড়ে নিয়ে “no preemption” শর্তটি ভাঙা যায় না কেন?
কেড়ে নেওয়া মানে শুধু সম্পদটি অন্যকে দেওয়া নয় — আগের মালিককে পরে ঠিক আগের অবস্থায় ফিরিয়ে দিতে পারতে হবে, আর মাঝখানে নতুন মালিক যেন কোনো অর্ধেক কাজ না দেখে।
CPU-র ক্ষেত্রে এটি সম্ভব, কারণ CPU-র পুরো “অবস্থা” কয়েকটি register-এ ধরা থাকে। context switch-এ (পর্ব ৯) OS সেগুলো PCB-তে সংরক্ষণ করে, পরে হুবহু ফিরিয়ে দেয়; প্রসেস টেরই পায় না। মেমরির পৃষ্ঠার ক্ষেত্রেও তাই — বিষয়বস্তু ডিস্কে লিখে রাখা যায়, পরে page fault-এ ফেরত আনা যায় (পর্ব ৩৫)।
Mutex-এর ক্ষেত্রে সমস্যাটি সম্পদে নয়, যে ডেটা সে পাহারা দেয় তাতে। একটি থ্রেড হয়তো একটি linked list-এর মাঝখানে একটি node সরাচ্ছিল — আগের node-এর pointer বদলেছে, পরেরটির বদলায়নি। এই মুহূর্তে lock কেড়ে অন্যকে দিলে সে ভাঙা list দেখবে। অর্থাৎ mutex কেড়ে নেওয়ার অর্থ হলো ডেটার সুসংগতির প্রতিশ্রুতি ভেঙে ফেলা। প্রিন্টার বা টেপ ড্রাইভের অবস্থা আরও খারাপ: অর্ধেক ছাপা কাগজ ফেরত নেওয়া যায় না।
তবে যেখানে “অর্ধেক কাজ মুছে ফেলার” ব্যবস্থা আছে, সেখানে preemption সত্যিই কাজ করে। ডেটাবেস ঠিক এটিই করে — wound-wait-এ বয়সে বড় transaction ছোটটির lock কেড়ে নেয়, আর ছোটটি rollback হয়ে নতুন করে শুরু করে। Hardware transactional memory (যেমন কিছু Intel প্রসেসরে TSX) একই ধারণা ব্যবহার করে: সংঘাত হলে CPU পুরো critical section-এর লেখা বাতিল করে আবার চালায়। Linux-এর OOM killer (পর্ব ৩৮)-ও একরকম চরম recovery — মেমরি ফুরিয়ে সিস্টেম আটকে গেলে একটি প্রসেসকে মেরে তার সব মেমরি “কেড়ে” নেয়, তার কাজের দাম দিয়ে।
মূল উপলব্ধি: preemption-এর আসল দাম হলো অবস্থা সংরক্ষণ বা কাজ বাতিলের ক্ষমতা। যে সম্পদের অবস্থা সস্তায় সংরক্ষণ করা যায় (CPU, পৃষ্ঠা) বা যে কাজ নিরাপদে বাতিল করা যায় (transaction), সেখানে এই শর্ত ভাঙা সহজ; বাকিগুলোতে অন্য শর্ত ভাঙতে হয়।
প্র ০৩Lock ordering-এর প্রমাণ এত সরল, তবু বড় সফটওয়্যারে lock-order বাগ বারবার আসে কেন?
প্রমাণটি সত্যিই নিখুঁত — নিয়ম মানলে চক্র অসম্ভব। কিন্তু সমস্যা হলো নিয়মটি মানা হচ্ছে কি না জানা, আর কিছু ক্ষেত্রে ক্রম আগে থেকে ঠিক করাই কঠিন।
- Lock লুকিয়ে থাকে: আপনি lock A ধরে একটি library ফাংশন ডাকলেন; সেটি ভেতরে lock B নেয়। অন্য জায়গায় কেউ B ধরে এমন কিছু ডাকে যা A নেয়। কোনো একটি ফাংশন পড়ে ভুল ধরা যায় না — চক্রটি ছড়িয়ে থাকে দুই দূরের কোড-পথে।
- Callback আর plug-in: lock ধরে রেখে অন্যের দেওয়া callback ডাকলে, সেই callback কী lock নেবে তা আপনার নিয়ন্ত্রণে নেই।
- গতিশীল বস্তু: দুটি হিসাব, দুটি inode, দুটি CPU-র run queue — কোনটি আগে, তা compile-এর সময় জানা যায় না। তখন id বা ঠিকানার ক্রম নিতে হয়, আর একই বস্তু দুবার আসার মতো কোণার ক্ষেত্রও সামলাতে হয় (rename-এর মতো অপারেশনে Linux দুটি directory lock করার আলাদা নিয়ম রাখে)।
- বিবর্তন: পাঁচ বছর আগে লেখা ক্রমের মন্তব্য নতুন ডেভেলপার পড়েননি; নতুন feature একটি নতুন lock আনল, যার জায়গা hierarchy-তে কেউ ঠিক করেনি।
এজন্যই Linux kernel ২০০৬ সালে lockdep যোগ করে। তার জোর হলো, সে deadlock ঘটার অপেক্ষা করে না — একবার A → B আর অন্যবার B → A ক্রম দেখলেই রিপোর্ট করে, এমনকি দুটি ঘটনা আলাদা দিনে, আলাদা থ্রেডে ঘটলেও। তাই এক-কোটিতে-একবার ঘটা deadlock-ও সাধারণ test চালিয়ে ধরা পড়ে। kernel-এর পরীক্ষার পরিকাঠামো (যেমন syzbot-এর মতো স্বয়ংক্রিয় fuzzing) নিয়মিত lockdep-এর রিপোর্ট বের করে। User-space-এ ThreadSanitizer আর helgrind একই কৌশলে lock-order-inversion রিপোর্ট করে।
আরেকটি ব্যবহারিক কৌশল হলো lock-এর সংখ্যা আর ব্যাপ্তি কমানো: একটি থ্রেড একবারে একটির বেশি lock না ধরলে চক্রের প্রশ্নই ওঠে না। অনেক নকশা এজন্য message passing বা একক মালিকানার থ্রেড (actor) বেছে নেয়।
মূল উপলব্ধি: সঠিক নিয়ম আর সেই নিয়মের প্রয়োগ আলাদা জিনিস। বড় সিস্টেমে প্রমাণের সাথে যন্ত্র দিয়ে যাচাই (lockdep, TSan) না থাকলে নিয়ম একসময় ভাঙবেই।
প্র ০৪Deadlock এড়াতে “trylock, না পেলে ছেড়ে দিয়ে আবার চেষ্টা” কৌশল নিলেন। এতে নতুন কোন বিপদ আসে, আর বাস্তবে তা কীভাবে সামলানো হয়?
এই কৌশল hold and wait ভাঙে — কেউ একটি lock ধরে রেখে অন্যটির জন্য ঘুমায় না। তাই deadlock সত্যিই অসম্ভব হয়। কিন্তু বদলে আসে livelock-এর ঝুঁকি।
দুই থ্রেড যদি একই তালে চলে — T1 নিল A, T2 নিল B; দুজনেই অন্যটির trylock-এ ব্যর্থ; দুজনেই ছেড়ে দিল; দুজনেই আবার একই মুহূর্তে শুরু করল — তাহলে চক্রটি চলতেই থাকবে। কেউ ঘুমিয়ে নেই, CPU পুরো ব্যস্ত, অথচ একটি লেনদেনও হচ্ছে না। একই core-এ বা একই timer-এ জাগা থ্রেডের ক্ষেত্রে এই তাল মেলা অবাক করার মতো সহজ।
সমাধান এসেছে নেটওয়ার্কিং থেকে। ১৯৭০-এর দশকে Ethernet-এর নকশাকারীরা একই সমস্যায় পড়েন: দুটি কম্পিউটার একসাথে পাঠালে সংঘর্ষ, আর দুজনে একই সময় পরে আবার পাঠালে আবার সংঘর্ষ। তাঁদের উত্তর randomized exponential backoff: প্রতিবার ব্যর্থ হলে একটি এলোমেলো সময় অপেক্ষা, আর ব্যর্থতা বাড়লে অপেক্ষার সর্বোচ্চ সীমা দ্বিগুণ। এলোমেলোতা তাল ভাঙে, আর দ্বিগুণ হওয়া ভিড়ের সাথে মানিয়ে নেয়। আজ ডেটাবেস deadlock-ত্রুটির পর retry, cloud API-র retry, এমনকি mutex-এর ভেতরের spin-then-sleep কৌশলে একই চিন্তা।
আরেকটি দাম হলো starvation: ভাগ্য খারাপ একটি থ্রেড বারবার ব্যর্থ হতে পারে, কারণ trylock কোনো সারি রাখে না। তাই চেষ্টার সংখ্যার সীমা দিয়ে, সীমা পেরোলে ক্রম-মানা blocking lock-এ ফিরে যাওয়া একটি প্রচলিত নকশা। আর সবচেয়ে সরল প্রতিকারটিও মনে রাখুন: lock-গুলো একই ক্রমে নিলে trylock-এর দরকারই পড়ে না।
মাত্রা বাড়লে একই ধরনের livelock kernel-এও দেখা যায় — বেশি প্যাকেট এলে interrupt সামলাতেই CPU শেষ হয়ে যাওয়ার “receive livelock”, যার প্রতিকারে Linux-এর NAPI বেশি load-এ polling-এ চলে যায়।
মূল উপলব্ধি: একটি শর্ত ভাঙলে deadlock যায়, কিন্তু অগ্রগতির নিশ্চয়তা আপনাআপনি আসে না। “কেউ আটকে নেই” আর “কেউ না কেউ এগোচ্ছে” — দুটি আলাদা প্রতিশ্রুতি, আর দ্বিতীয়টির জন্য এলোমেলোতা, সীমা বা ক্রম লাগে।
প্র ০৫একাধিক সার্ভারে ছড়ানো সিস্টেমে deadlock ধরা এক মেশিনের চেয়ে এত কঠিন কেন, আর বড় সিস্টেমগুলো আসলে কী করে?
এক মেশিনে wait-for graph একটি মেমরিতে থাকে, এক মুহূর্তে পুরোটা দেখা যায়। distributed সিস্টেমে প্রতিটি সার্ভার শুধু নিজের অংশটুকু জানে, আর অন্যদের খবর আসে নেটওয়ার্কে, দেরিতে। এতে তিনটি সমস্যা তৈরি হয়।
- চক্র ভাগ হয়ে থাকে: T1 সার্ভার X-এ T2-র অপেক্ষায়, T2 সার্ভার Y-এ T1-এর অপেক্ষায়। X বা Y কেউই একা চক্র দেখে না।
- পুরোনো ছবি: একটি কেন্দ্রীয় সমন্বয়কারী সবার graph জোগাড় করলে, বার্তাগুলো ভিন্ন ভিন্ন মুহূর্তের। ইতিমধ্যে খুলে যাওয়া অপেক্ষা আর নতুন অপেক্ষা মিলিয়ে সে এমন চক্র দেখতে পারে যা কখনো একসাথে ছিল না — phantom deadlock, যাতে অকারণে একটি transaction বাতিল হয়।
- কেন্দ্রীয় সমন্বয়কারী নিজেই দুর্বলতা: সে বন্ধ হলে detection বন্ধ, আর সব বার্তা তার কাছে যাওয়ায় ভিড়।
গবেষণার উত্তর ছিল edge-chasing: ১৯৮৩ সালে Chandy, Misra ও Haas প্রস্তাব করেন, অপেক্ষারত প্রসেস যার জন্য অপেক্ষা করছে তাকে একটি ছোট “probe” পাঠাবে, সে আবার নিজের অপেক্ষার দিকে এগিয়ে দেবে; probe ঘুরে শুরুর প্রসেসেই ফিরলে চক্র নিশ্চিত — পুরো graph কাউকে জোগাড় করতে হয় না।
তবে বাস্তবের বড় সিস্টেম প্রায়ই detection-ই এড়িয়ে যায়। দুটি জনপ্রিয় পথ: (১) timeout — নির্দিষ্ট সময়ের বেশি অপেক্ষা করলে transaction বাতিল করে আবার চেষ্টা; সরল, কিন্তু ধীর অথচ সুস্থ কাজও বাতিল হতে পারে। (২) timestamp-ভিত্তিক prevention — wait-die বা wound-wait, যেখানে বয়স দিয়ে ঠিক হয় কে অপেক্ষা করবে আর কে বাতিল হবে, ফলে চক্রই তৈরি হয় না। Google-এর Spanner-এর প্রকাশিত নকশায় read-write transaction wound-wait ব্যবহার করে। এগুলোর বিস্তারিত DBMS কোর্সের concurrency control পর্বে আছে।
Microservice-এ আরেকটি পরিচিত প্রতিকার: প্রতিটি নেটওয়ার্ক-কলে deadline, আর ভিন্ন ধরনের অনুরোধের জন্য আলাদা থ্রেড-পুল (bulkhead), যাতে A → B → A ধরনের কল A-র সব থ্রেড আটকে না ফেলে।
মূল উপলব্ধি: distributed সিস্টেমে “পুরো ছবি” কখনো নিখুঁতভাবে একসাথে পাওয়া যায় না। তাই সেখানে নকশাকারীরা এমন নিয়ম পছন্দ করেন, যা স্থানীয় তথ্য দিয়েই চক্র অসম্ভব করে — ঠিক lock ordering-এর মতো।
অনুশীলন
কাগজ-কলম নিন — RAG আঁকা আর হিসাব হাতে করাই আসল অনুশীলন।
-
RAG এঁকে রায় দিন: R1, R2, R3 প্রত্যেকের ১টি instance. P1 ধরে আছে R1, চাইছে R2; P2 ধরে আছে R2, চাইছে R3; P3 ধরে আছে R3. (ক) deadlock আছে? (খ) এবার P3 R1 চাইল — এখন? (গ) (খ)-এর অবস্থায় R1-এর যদি ২টি instance থাকত, আর দ্বিতীয়টি থাকত P4-এর হাতে, যে কিছু চায় না — তখন?
(ক) তীর: R1 → P1, P1 → R2, R2 → P2, P2 → R3, R3 → P3. শিকল P1 → P2 → P3, কিন্তু P3 কিছুর অপেক্ষায় নেই — চক্র নেই, deadlock নেই। P3 শেষ করলে P2, তারপর P1.
(খ) নতুন তীর P3 → R1. চক্র P1 → R2 → P2 → R3 → P3 → R1 → P1, আর সব single-instance — deadlock নিশ্চিত, P1, P2, P3 তিনজনই। wait-for graph: P1 → P2 → P3 → P1.
(গ) চক্রটি এখনো আছে, কিন্তু R1 multi-instance. Reduction: P4-এর কোনো অনুরোধ নেই, সে শেষ করে তার R1 ছাড়ে; সেটি P3 পায়, P3 শেষ করে R3 আর R1 ছাড়ে; P2 R3 পেয়ে শেষ করে R2 ছাড়ে; P1 R2 পায়। deadlock নেই — চক্র থাকলেও।
-
Numerical: (ক) ৫টি প্রসেস, প্রত্যেকের সর্বোচ্চ ৩টি টেপ ড্রাইভ লাগে। deadlock-মুক্ত থাকতে কমপক্ষে কতটি ড্রাইভ? (খ) ৮টি ড্রাইভ থাকলে ৩টি-চাওয়া প্রসেস সর্বোচ্চ কয়টি নিরাপদে চলতে পারে? (গ) চাহিদা ২, ৩, ৪ আর ৫ হলে কমপক্ষে কতটি? প্রতিটির জন্য “একটি কম হলে কীভাবে আটকায়” দেখান।
(ক) ৫ × (৩ − ১) + ১ = ১১. ১০টি হলে প্রত্যেকে ২টি করে নিয়ে সব শেষ — সবাই তৃতীয়টির অপেক্ষায়।
(খ) n × ২ + ১ ≤ ৮ ⇒ n ≤ ৩.৫ ⇒ ৩টি. ৩টি প্রসেসে সবচেয়ে খারাপ অবস্থায় ৬টি বরাদ্দ, ২টি মুক্ত — কেউ না কেউ তৃতীয়টি পাবে। ৪টি প্রসেসে ৪ × ২ = ৮, সব শেষ, সবাই আটকে।
(গ) ১ + ২ + ৩ + ৪ + ১ = ১১. ১০টি হলে হাতে [1, 2, 3, 4] — সবাই ঠিক একটি কম পেয়ে অপেক্ষায়।
-
কোড লিখুন: অংশ ৭-এর
lock_pair-এ একটি লুকানো বাগ আছে —lock_pair(&acc[0], &acc[0])ডাকলে (অর্থাৎfromআরtoএকই হিসাব হলে) কী হবে? বাগটি ঠিক করেlock_pairআর একটি মিলে যাওয়াunlock_pairলিখুন। বোনাস: id না থাকলে কোন ক্রম ব্যবহার করবেন?একই হিসাব হলে
x == y, তাই একই default mutex দুবার lock হয় — থ্রেড নিজের হাতের lock-এর জন্যই অপেক্ষা করে, অর্থাৎ এক থ্রেডের self-deadlock (default mutex-এ আচরণ undefined; বাস্তবে সাধারণত চিরকাল আটকে থাকে)। সমাধান:static void lock_pair(struct account *x, struct account *y) { if (x == y) { pthread_mutex_lock(&x->m); return; } /* same account: one lock */ if (x->id > y->id) { struct account *t = x; x = y; y = t; } pthread_mutex_lock(&x->m); pthread_mutex_lock(&y->m); } static void unlock_pair(struct account *x, struct account *y) { pthread_mutex_unlock(&x->m); if (x != y) pthread_mutex_unlock(&y->m); }id না থাকলে ঠিকানার ক্রম:
(uintptr_t)x < (uintptr_t)yহলে আগে x. বস্তুগুলো সরে না গেলে ঠিকানা স্থায়ী, তাই ক্রমও স্থায়ী। ব্যাংকের আসল লেনদেনেfrom == toহলে লেনদেনটি বাতিল করাই হয়তো ভালো নিয়ম — কিন্তু lock-এর ফাংশনকে তবু নিরাপদ রাখা উচিত। -
কোনটি কোন অসুখ? প্রতিটিকে deadlock, livelock বা starvation বলুন, এক লাইনে কারণসহ: (ক) দুই থ্রেড trylock দিয়ে দুটি lock নিতে গিয়ে বারবার ব্যর্থ হয়ে ছেড়ে দিচ্ছে, CPU ১০০%. (খ) একটি কম-priority প্রসেস ঘণ্টার পর ঘণ্টা CPU পাচ্ছে না, কারণ উচ্চ-priority কাজ আসতেই থাকে। (গ) দুটি প্রসেস pipe দিয়ে কথা বলে; দুজনেই অন্যজনের বার্তার জন্য
read-এ আটকে, CPU ০%. (ঘ) উপরের (গ)-তে একটি প্রসেস প্রতি সেকেন্ডে timeout হয়ে বার্তা না পাঠিয়ে আবারread-এ ফিরছে।(ক) Livelock — সবাই চলছে, অবস্থা বদলাচ্ছে, কিন্তু অগ্রগতি নেই; প্রতিকার random backoff বা একই lock-ক্রম।
(খ) Starvation — অন্যরা এগোচ্ছে, শুধু এটি পালা পাচ্ছে না; প্রতিকার aging (পর্ব ১৮)।
(গ) Deadlock — consumable সম্পদ (বার্তা) নিয়ে চক্র; দুজনেই এমন ঘটনার অপেক্ষায় যা কেবল অন্যজন ঘটাতে পারে, আর দুজনেই ঘুমিয়ে।
(ঘ) এখনো কার্যত deadlock (কেউ কেউ livelock বলবেন) — প্রসেসটি জেগে উঠছে বটে, কিন্তু যে ঘটনা জট খুলবে (অন্তত একজনের লেখা) তা কখনো ঘটছে না। timeout একা প্রতিকার নয়; timeout-এর পর আচরণ বদলাতে হবে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমদুই থ্রেড উল্টো ক্রমে দুটি mutex নিলে deadlock কীভাবে তৈরি হয় — ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- T1 mutex A নিল
- T2 mutex B নিল
- T1 B চাইল — B T2-র কাছে, তাই T1 ঘুমাল
- T2 A চাইল — A T1-এর কাছে, তাই T2 ঘুমাল
- wait-for graph-এ চক্র T1 → T2 → T1; কেউ আর কখনো জাগবে না
নিচের কোনটি deadlock-এর চারটি প্রয়োজনীয় (Coffman) শর্তের একটি নয়?
- Mutual exclusion
- Hold and wait
- Circular wait
- Bounded waiting
একটি resource-allocation graph-এ চক্র আছে, আর graph-এর প্রতিটি resource type-এর ঠিক একটি করে instance. সিদ্ধান্ত কী?
- Deadlock হতেও পারে, না-ও হতে পারে
- চক্রের প্রসেসগুলো নিশ্চিতভাবে deadlocked
- কোনো deadlock নেই
- Banker's algorithm না চালিয়ে কিছু বলা যায় না
একটি সিস্টেমে ৪টি প্রসেস, প্রত্যেকের একই ধরনের সম্পদের সর্বোচ্চ ৩টি একক লাগে (একক একটি একটি করে চাওয়া হয়)। deadlock কখনো না হওয়া নিশ্চিত করতে কমপক্ষে কতটি একক লাগবে?
- ৮
- ১২
- ৯
- ৭
সব থ্রেড একটি নির্দিষ্ট global ক্রমে (যেমন id-র ক্রমে) lock নিলে deadlock-এর কোন শর্তটি ভাঙে?
- Circular wait
- Mutual exclusion
- Hold and wait
- No preemption
R1 আর R2 দুটি resource type, প্রত্যেকের ২টি instance. RAG-এ চক্র P1 → R1 → P3 → R2 → P1 আছে, আর R1-এর অন্য instance P2-এর হাতে ও R2-এর অন্যটি P4-এর হাতে; P2 আর P4 কিছু চাইছে না। সিদ্ধান্ত কী?
- চক্র আছে, তাই P1 আর P3 deadlocked
- Deadlock নেই — চক্রের বাইরের প্রসেস শেষ করলে চক্র ভেঙে যায়
- চারটি প্রসেসই deadlocked
- তথ্য যথেষ্ট নয়, কিছুই বলা যায় না
দুটি থ্রেড বারবার lock নিচ্ছে, দ্বিতীয়টি না পেয়ে ছেড়ে দিচ্ছে, আবার চেষ্টা করছে; CPU ব্যবহার বেশি, কিন্তু কোনো কাজ এগোচ্ছে না। এটি কী?
- Deadlock
- Starvation
- Livelock
- Priority inversion
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Deadlock = একদল প্রসেসের প্রত্যেকে দলেরই অন্য কারও হাতের সম্পদের অপেক্ষায়; সম্পদ ব্যবহার হয় request → use → release ক্রমে।
- চারটি শর্ত একসাথে লাগে: mutual exclusion, hold and wait, no preemption, circular wait; single-instance-এ এগুলো যথেষ্টও।
- RAG-এ চক্র নেই ⇒ deadlock নেই; চক্র + single instance ⇒ deadlock; চক্র + multiple instance ⇒ reduction বা detection করে দেখতে হবে।
- বাস্তবের সেরা প্রতিরোধ global lock order — প্রমাণ: শিকল ধরে rank বাড়ে, তাই চক্র ফিরতে পারে না; lockdep আর TSan নিয়ম ভাঙা ধরে।
- একটি টাইপের সম্পদে n(k − 1) + 1 একক থাকলে deadlock অসম্ভব; আর deadlock, livelock ও starvation তিনটি আলাদা অসুখ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৮ · Deadlock avoidance আর detection — Banker's algorithmপরবর্তী পর্বSafe state, safety algorithm আর matrix দিয়ে detection.
- অপারেটিং সিস্টেম · কোর্স হোম৬৪টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- DBMS · Concurrency controlপাশের ট্র্যাকডেটাবেসের deadlock — wait-for graph, victim, wait-die আর wound-wait.
- DSA · DFSপাশের ট্র্যাকDirected graph-এ চক্র খোঁজার algorithm, যা deadlock detection-এর ভিত্তি।
- C প্রোগ্রামিংভিত্তিpointer, struct আর মেমরি — pthreads-এর কোড বোঝার ভিত্তি।