3.3
Thread Pool ও Deadlock
Thread বারবার ব্যবহার, pool কত বড় হবে, deadlock-এর চারটা শর্ত আর সেগুলো ভাঙার উপায়।
55 মিনিট পড়া + Animation
টিপস: ডটেড-আন্ডারলাইন করা যেকোনো শব্দে ট্যাপ বা Hover করলে তার অর্থ দেখবে।
এই Lesson শেষে তুমি পারবে
- Thread Pool কী, Server কেন এটা ব্যবহার করে আর এর দাম কী, ১০,০০০ Task আর ২০ Worker-এর হিসাব ধরে বোঝাতে।
- CPU-bound আর I/O-bound কাজের জন্য Pool-এর মাপ ঠিক করতে, আর ধরতে পারতে কখন আসল সীমা CPU না, Database।
- সীমিত Queue আর Rejection Policy বাছতে, আর Java-র
ThreadPoolExecutorকোন ক্রমে বড় হয় তা আগে থেকে বলতে। - লক্ষণ দেখে Pool Exhaustion চিনতে, আর Timeout ও Bulkhead দিয়ে ক্ষতি আটকাতে।
- কোনো Lock ছাড়াই একটা Pool কীভাবে নিজের সাথে Deadlock করে, তা বোঝাতে আর ঠিক করতে।
- Coffman-এর চারটা শর্ত বলতে, যেকোনো দৈর্ঘ্যের চক্র আঁকতে, আর লম্বা Wait Chain থেকে Deadlock আলাদা করতে।
- Lock Ordering আর অন্য কৌশল দিয়ে Deadlock ঠেকাতে, আর কোনটা Prevention, কোনটা Recovery তা বলতে।
- Wait-for Graph, Thread Dump আর
pg_locksদিয়ে Deadlock খুঁজে বের করতে, আর Database-এর Deadlock Victim-এর জন্য ঠিকঠাক Retry লিখতে। - Dashboard-এ যা দেখছ তা থেকে Deadlock, Starvation, Livelock আর Priority Inversion আলাদা করতে।
#এই Lesson কোথায় বসে
Lesson ১.২-এ Connection প্রতি একটা Thread-এর Server দেখেছি, আর Thread Pool Exhaustion-এর নামও শুনেছি। Lesson ১.১৩-এ একই নকশা দেখেছি PHP-FPM, Gunicorn আর Tomcat-এ, সাথে Worker ফুরিয়ে গেলে আসা 504-ও। Lesson ২.৫-এ দেখেছি লম্বা সময় খোলা থাকা Connection কীভাবে একটা করে Thread আটকে রাখতে পারে।
এই Chapter-এই Lesson ৩.১-এ জেনেছি Thread কী আর তৈরি করতে কত খরচ, আর Lesson ৩.২-এ জেনেছি Scheduling কীভাবে চলে, আর বেশি Thread কেন CPU নষ্ট করে। ওখান থেকে একটা প্রশ্ন বাকি থেকে গেছে, এই Lesson তার উত্তর: অল্প কয়টা Thread দিয়ে অনেক কাজ চালাবে কীভাবে, আর Thread-রা একে অপরের জন্য অপেক্ষা করলে কী কী গোলমাল হয়?
Event Loop-এর পথটা আছে Lesson ৩.৬-এ। আসল Code-এ Lock (Mutex, Semaphore, Race, CAS) আসবে Chapter 14-এ; এখানে আমরা অপেক্ষা আর Deadlock-কে দেখব পুরো System-এর আচরণ হিসেবে।
#Thread Pool জিনিসটা কী
Thread Pool হলো আগে থেকে তৈরি করে রাখা কিছু Worker Thread, যারা একটা Queue থেকে Task নিয়ে চালায়। Producer Task রেখে যায়; Worker তুলে নেয়, চালায়, তারপর পরেরটার জন্য ফিরে আসে।
Incoming tasks
|
v
Task queue
/ | \
v v v
Worker Worker Worker
thread thread thread
ধরো একটা Application-এ ১০,০০০ Task এল। প্রতিটার জন্য নতুন Thread না বানিয়ে তুমি ২০টা Worker Thread দিয়ে কাজগুলো করালে। একটা Task-এ ৫০ ms লাগলে:
- ২০টা Worker সেকেন্ডে শেষ করে ২০ ÷ ০.০৫ s = ৪০০ Task।
- ১০,০০০ Task শেষ হয় ১০,০০০ ÷ ৪০০ = ২৫ সেকেন্ডে।
- Thread কখনোই ২০টার বেশি হয় না, তাই তৈরির খরচ আর Context Switch থাকে হাতের মুঠোয়।
দামটা হলো, ৫০ ms-এর কাজের জন্য শেষ Task-টা প্রায় ২৫ সেকেন্ড বসে ছিল। Pool আসলে অতিরিক্ত Concurrency-কে Queue-র অপেক্ষায় বদলে দেয়। এই লেনদেনটা সাধারণত ঠিকই আছে, কারণ Queue ধীরে ধীরে, চোখের সামনে খারাপ হয়, আর ১০,০০০ Thread পুরো Machine-কেই বসিয়ে দিতে পারে।
Thread Pool: একটা Queue, নির্দিষ্ট সংখ্যক Worker
প্রতিটা ঘর একটা Thread বা Queue-তে থাকা একটা Task। দেখো ১০,০০০ Task কীভাবে ২০টা Worker দিয়ে পার হয়, Task প্রতি একটা Thread-এর সাথে মিলিয়ে নাও, তারপর স্বাভাবিক Traffic আর হঠাৎ ধীর হয়ে যাওয়া Dependency দেখো।
পরিস্থিতি
প্রতিটা Task-এ ৫০ ms লাগে, তাই কতগুলো অপেক্ষায় আছে তা যা-ই হোক, ২০টা Worker সেকেন্ডে ৪০০টা Task শেষ করে।
Pool তার ২০টা Thread একবারই তৈরি করে। কাজ না থাকলে এরা Queue-র ওপর ঘুমিয়ে থাকে, CPU খায় না, আর জেগে উঠতে লাগে মাত্র কয়েক Microsecond।
#সুবিধা আর সমস্যা
| সুবিধা | কেন |
|---|---|
| Thread তৈরির খরচ কমে | Thread বারবার ব্যবহার হয়, তাই System Call আর Stack বানানো একবারই |
| Resource সামলানো সহজ | Stack-এর Memory আর Scheduler-এর চাপের একটা জানা ছাদ থাকে |
| Concurrency সীমিত থাকে | একসাথে সর্বোচ্চ N-টা Task, এতে এরা যাদের Call করে তারাও বাঁচে |
| Queue দিয়ে Load সামলানো যায় | ছোট ধাক্কা অপেক্ষা করে; লম্বা Overload ফিরিয়ে দেওয়া যায় |
| সমস্যা | দেখতে কেমন |
|---|---|
| Queue বড় হলে Latency বাড়ে | Task চলার চেয়ে অপেক্ষাতেই বেশি সময় কাটায় |
| সব Thread আটকে গেলে | Thread Starvation: CPU অলস, অথচ Queue বাড়ছে |
| Pool-এর মাপ ভুল | ছোট হলে Hardware নষ্ট; বড় হলে CPU নষ্ট, আর পেছনের Service-এর ওপর চাপ |
| মেশানো কাজে একটাই Setting | CPU-bound আর I/O-bound-এর মাপ আলাদা, আর ধীর Task দ্রুত Task-কে আটকে রাখে |
#ব্যবহার করবে কীভাবে
Java-তে সবচেয়ে সহজ রূপটা এই:
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {
System.out.println("Task running");
});
executor.shutdown(); // finish queued tasks, accept no new ones
Demo-র জন্য ঠিক আছে, কিন্তু এর Queue-র কোনো সীমা নেই, আর সেটা সত্যিকারের বিপদ (একটু পরেই দেখবে)। Production-এ Setting-গুলো খুলে লেখো:
ThreadPoolExecutor pool = new ThreadPoolExecutor(
20, 20, // core and maximum threads
0L, TimeUnit.MILLISECONDS, // keep-alive for extra threads
new ArrayBlockingQueue<>(200), // bounded queue
new ThreadPoolExecutor.AbortPolicy()); // reject when full
Thread Pool আসলে Producer-Consumer নকশাই, যেখানে Data হলো Task: Request Handler বানায়, Worker খায়, আর মাঝের Queue দুজনের গতি আলাদা করে রাখে। পার্থক্য শুধু জোরের জায়গায়। Producer-Consumer বলে Data Pipeline-এর কথা; Thread Pool বলে গোছানো কিছু Thread-এ Task চালানোর কথা।
#Pool তুমি আগেই কোথায় দেখেছ
| System | Pool | সাধারণ Setting |
|---|---|---|
| Tomcat | Request Thread | maxThreads, ডিফল্ট ২০০ |
| PHP-FPM | Worker Process (Process-এর Pool) | pm.max_children, তুমি ঠিক করো |
| Gunicorn | Worker Process, চাইলে Thread-সহ | --workers, --threads |
Python ThreadPoolExecutor | Worker Thread | ডিফল্ট min(32, cores + 4) |
| Node.js libuv | File আর DNS কাজের Worker | ডিফল্টে ৪টা (Lesson ৩.৬) |
Java ForkJoinPool Common Pool | Parallel Stream-এর Work-stealing Worker | Core − ১ |
| HikariCP, PgBouncer | Database Connection, Thread না | একই ধারণা হুবহু খাটে |
শেষ সারিটা খেয়াল করো। এই Lesson-এর সব কথা যেকোনো দুর্লভ জিনিসের Pool-এই খাটে: Connection, Process, Memory Buffer।
#কয়টা Thread লাগবে?
সবার জন্য একটা সংখ্যা নেই, কিন্তু ভাবার একটা ভালো রাস্তা আছে। যে Thread I/O-র জন্য অপেক্ষা করে সে তার Core ছেড়ে দেয়। তাই উত্তরটা নির্ভর করে প্রতিটা Task-এর কতটুকু অপেক্ষায় কাটে তার ওপর।
CPU-bound কাজ (Image Resize, Compression, Hashing) অপেক্ষাই করে না, একটা Thread একটা Core পুরো ভরে ফেলে:
Thread ≈ Core
I/O-bound কাজ (যে Web Request বেশিরভাগ সময় Database-এর দিকে তাকিয়ে থাকে) অপেক্ষার সময় Core ফাঁকা রাখে, তাই বেশি Thread মিলে সেটা ভাগ করে নিতে পারে:
Thread ≈ Core × (১ + অপেক্ষার সময় ÷ হিসাবের সময়)
Java Concurrency in Practice বইয়ের রূপে আরেকটা জিনিস আছে, ০ থেকে ১-এর মধ্যে একটা লক্ষ্য Utilisation U: Core × U × (১ + অপেক্ষা ÷ হিসাব)।
একটা হিসাব করে দেখি: ৪টা Core, আর প্রতিটা Request ১০ ms হিসাব করে, ৯০ ms অপেক্ষা করে।
| Thread | Throughput | CPU | কী ঘটছে |
|---|---|---|---|
| ৪ | ৪ ÷ ০.১ s = ৪০/s | প্রায় ১০% | Core অলস; যে Hardware-এর টাকা দিচ্ছ তা কাজে লাগছে না |
| ২৮ (U = ০.৭) | ২৮০/s | ৭০% | ভালো: ধাক্কা সামলানোর জায়গা আছে |
| ৪০ (সূত্র) | ৪০০/s | ১০০% | সর্বোচ্চ, কিন্তু বাড়তি জায়গা নেই, তাই Queue জমে |
| ১০০ | তবু ৪০০/s | ১০০% | Thread Core-এর লাইনে: Latency বেশি, Throughput একই |
কয়টা Thread? Core-এর দিকে তাকাও
প্রতিটা Scenario-তে চারটা Core। যে Thread I/O-র জন্য অপেক্ষা করে সে তার Core ছেড়ে দেয়, তাই ঠিক সংখ্যাটা নির্ভর করে প্রতিটা Task-এর কতটুকু অংশ অপেক্ষায় কাটে তার ওপর।
পরিস্থিতি
Image Resize: পুরোটাই হিসাব, কোনো অপেক্ষা নেই। Thread ≈ Core।
চার Core-এ চারটা Thread। প্রতিটা Core সবসময় কাজে, কাউকে অপেক্ষা করতে হয় না। CPU-bound Task এক সময়ে একটার বেশি Core নিতে পারে না, তাই এটাই সর্বোচ্চ।
সূত্রটা শুরু করার জায়গা মাত্র, আর সে তাকায় শুধু CPU-র দিকে। আরও চারটা জিনিস মিলিয়ে নাও:
১. পরের ধাপের সবচেয়ে সরু Resource। ৪০টা Thread আর ১০টা Database Connection থাকলে, আর প্রতিটা Connection ৯০ ms ধরে রাখা হলে, ছাদ হলো ১০ ÷ ০.০৯ s ≈ সেকেন্ডে ১১১ Request। বাকি Thread-রা Connection-এর লাইনে দাঁড়িয়ে Memory খায়, আর আসল সীমাটা ঢেকে রাখে।
২. Little-এর সূত্র দিয়ে মিলিয়ে দেখা। ব্যবহৃত Thread = আসার হার × প্রতি Request-এর সময়। সেকেন্ডে ৫০০ Request, ০.১ s করে, মানে ৫০টা Thread। প্রতিটায় ২ s লাগলে ১,০০০টা। Lesson ১.২-এ এটা ব্যবহার করেছি, পুরোটা আসবে Chapter 17-এ।
৩. “Core” আসলে কতগুলো। CPU Limit-ওয়ালা Container-এ Host-এর Core না, Limit-টা গোনো (Lesson ৩.২-এ Throttling আছে)। Hyper-thread-ও পুরো Core না। আর এক Process-এর কয়েকটা Pool একই Core ভাগ করে খায়, তাই সবগুলোর যোগফল মিলতে হবে।
৪. মেপে পাওয়া ফল। Load Test চালাও, আর মাপ বাড়াতে থাকো যতক্ষণ না Throughput থেমে যায় বা Latency লাফ দেয়। সূত্র যা-ই বলুক, তোমার সংখ্যা ওটাই।
Database Connection Pool-এর জন্য PostgreSQL Community-র শুরুর হিসাব, যা HikariCP-র Documentation-এও আছে: Connection ≈ (Core × ২) + কার্যকর Disk-এর সংখ্যা, আর এই Core হলো Database Server-এর। সংখ্যাটা প্রায়ই অবাক করার মতো ছোট হয়। Active Connection বেশি হলে Database-এর সময় যায় Lock আর Scheduling-এর ঠেলাঠেলিতে, আর সবার জন্যই সে ধীর হয়ে পড়ে।
#আলাদা কাজের জন্য আলাদা Pool
ধীর আর দ্রুত Task, বা CPU-bound আর I/O-bound Task, আলাদা Pool-এ রাখো। Shared Pool-এ অল্প কয়টা লম্বা Task সব Worker নিয়ে নেয়, আর ছোট Task-রা পেছনে বসে থাকে। Core সংখ্যার সমান একটা CPU Pool, আর সূত্র মেনে একটা I/O Pool থাকলে, দুটোকে আলাদা করে সাজানো আর নজরে রাখা যায়।
#Queue: সীমিত, আর ভরে গেলে কী করবে
Overload-এ Pool কেমন আচরণ করবে, তা ঠিক হয় Queue-তে। সীমাহীন Queue কখনো “না” বলে না, তাই Overload প্রথমে দেখা দেয় Memory বাড়া আর ভয়ংকর Latency হিসেবে, তারপর Crash হিসেবে, যাতে Queue-র সবকিছু হারায়। সীমিত Queue Overload-কে একটা স্পষ্ট সিদ্ধান্তে বদলে দেয়।
লম্বা কতটা হবে? ঠিক করো একটা Task কতক্ষণ অপেক্ষা করলে সেটার আর মানে থাকে:
Queue-র দৈর্ঘ্য ≈ Throughput × যতটুকু অপেক্ষা চলে
সেকেন্ডে ৪০০ Task, Client প্রায় এক সেকেন্ডে হাল ছেড়ে দেয়, আর তুমি অপেক্ষায় আধা সেকেন্ড খরচ করতে পারো। তাহলে ৪০০ × ০.৫ = ২০০। এর পেছনে যা থাকবে তা শেষ হবে যখন আর কারও দরকার নেই। তার চেয়ে এখনই ফিরিয়ে দেওয়া ভালো।
Java-র ThreadPoolExecutor যে ক্রমে বড় হয়, তা বেশিরভাগ মানুষকে অবাক করে:
১. Core-এর চেয়ে কম Thread চলছে: নতুন Thread চালু করো।
২. নইলে: Task Queue-তে রাখো।
৩. Queue ভরা: Maximum পর্যন্ত বাড়তি Thread চালু করো।
৪. Maximum-ও ভরা: Task ফিরিয়ে দাও।
মানে বাড়তি Thread চালু হয় Queue ভরার পরে। Queue খুব বড় বা সীমাহীন হলে Maximum-এর Setting কোনো কাজেই আসে না। Executors.newFixedThreadPool-এর Queue সীমাহীন, আর Executors.newCachedThreadPool করে ঠিক উল্টোটা: Queue-তে কোনো জায়গাই নেই, Thread-এরও বাস্তবে কোনো সীমা নেই, তাই একটা ধাক্কায় হাজার হাজার Thread তৈরি হতে পারে। Tomcat-এর Request Pool এখানে ব্যতিক্রম: নিজের Executor দিয়ে সে আগে Thread বাড়ায়, পরে Queue-তে রাখে।
| Rejection Policy | কী হয় | কোথায় মানায় | বিপদ |
|---|---|---|---|
AbortPolicy (Java-র ডিফল্ট) | Caller-এর দিকে RejectedExecutionException ছোড়ে | Request Handling: Retry-After-সহ 503 বানাও | Caller-কে এটা সত্যিই সামলাতে হবে |
CallerRunsPolicy | যে জমা দিচ্ছে, সে-ই Task চালায় | Batch Job: আপনাআপনি Backpressure, কিছু হারায় না | Caller যদি Event Loop বা Connection নেওয়ার Thread হয়, সর্বনাশ |
DiscardPolicy | নতুন Task চুপচাপ ফেলে দেয় | Metric-এর নমুনা, যা হারালে ক্ষতি নেই | চুপচাপ Data হারায় |
DiscardOldestPolicy | Queue-র সবচেয়ে পুরনোটা ফেলে দেয় | সর্বশেষ মানই যেখানে কাজের, পুরনোটা মূল্যহীন | চুপচাপ Data হারায়, জরুরি কিছুও যেতে পারে |
সীমিত Queue, আর সেটা ভরে গেলে কী হয়
একটা Java ThreadPoolExecutor: ২টা Core Thread, সর্বোচ্চ ৪টা, Queue-তে ৪টা জায়গা। এটা যে ক্রমে বড় হয় তা বেশিরভাগ মানুষকে অবাক করে, আর Rejection Policy ঠিক করে Overload-এর ধাক্কা কার গায়ে লাগবে।
পরিস্থিতি
নয়টা ধীর Task একটার পর একটা আসে।
Task ১ আর ২: চলমান Thread Core সংখ্যার চেয়ে কম, তাই প্রতিটার জন্য Pool একটা নতুন Thread চালু করে।
Production থেকে আরও তিনটা নিয়ম:
- বেশি পুরনো Task ফেলে দাও। যে Request-এর Client ১০ সেকেন্ড আগে Timeout হয়ে গেছে, Worker-এর উচিত সেটা বাদ দেওয়া। Task-এর সাথে একটা Deadline রাখো, আর আগে সেটা দেখে নাও।
- Memory-র Queue Process-এর সাথেই মরে। Task যদি Crash বা Deploy-এর পরেও টিকে থাকতে হয় (Email, Payment), Queue থাকা উচিত টেকসই কোনো System-এ। Chapter 30 থেকে 32-এ এটা আছে।
- ফিরিয়ে দেওয়াটা একটা Feature। অল্প কয়েকজনের দ্রুত ব্যর্থতা বাকিদের বাঁচায়। Chapter 29 আর Chapter 28-এ Rate Limiting আর Load Shedding এই ধারণার ওপরেই দাঁড়ানো।
#Pool Exhaustion: সেই চিরচেনা Incident
Production-এর সবচেয়ে সাধারণ সমস্যাগুলোর একটা হলো Thread Pool Exhaustion। অদ্ভুত লাগে এই কারণে যে কিছুই নষ্ট মনে হয় না: Crash নেই, CPU কম, অথচ সব ধীর।
ঘটনাক্রম প্রায় সবসময় একই। একটা Dependency ধীর হলো। তাকে Call করা Thread-গুলো আর ফিরছে না। আটকে থাকা Thread বাড়ে আসার হার × ধরে রাখার সময় হারে, তাই সেকেন্ডে ৫০ Request আর ৩০ s Timeout মিলে ১,৫০০ Thread পর্যন্ত আটকে রাখতে পারে, ২০০-র Pool-এর চেয়ে অনেক বেশি। কয়েক সেকেন্ডে Pool ভরে যায়, আর তখন সব Endpoint Queue-তে দাঁড়ায়, এমনকি যারা ওই Dependency ছোঁয়ও না। তারপর Retry-তে চাপ বাড়ে, Health Check ফেল করে, Load Balancer Instance সরিয়ে দেয়, আর তাদের Traffic পড়ে বাকিদের ঘাড়ে।
একটা ধীর Dependency সব Endpoint বসিয়ে দেয়
একটা API, ২০০ Thread-এর একটাই Shared Pool। Search-এ সেকেন্ডে ৪৫০ Request আসে, ০.১ s লাগে। Checkout-এ আসে সেকেন্ডে ৫০টা, সে Payment Provider-কে Call করে। Provider এবার ঝুলে যাওয়া শুরু করল।
পরিস্থিতি
HTTP Client-এর Timeout ৩০ s। Payment নষ্ট হয়েছে, অথচ টের পাচ্ছে Search-এর ব্যবহারকারীরা।
স্বাভাবিক দিন: Search নেয় ৪৫০ × ০.১ s = ৪৫ Thread, Checkout নেয় ৫০ × ০.২ s = ১০। জায়গার কোনো অভাব নেই।
চিনবে কীভাবে, আর কোন লক্ষণ কী বাদ দেয়:
| লক্ষণ | মানে |
|---|---|
| Latency বেশি, CPU কম | Thread অপেক্ষা করছে, কাজ করছে না। এটা CPU-র সমস্যা না |
| Active Thread = সর্বোচ্চ, Queue বাড়ছে | Pool ভরা |
| Thread Dump: বেশিরভাগ Thread একই Stack Frame-এ | ওই Frame-টাই ধীর Dependency |
| Proxy-তে 504, বা Connection হয় কিন্তু উত্তর আসে না | App Connection নিচ্ছে, কিন্তু সামলানোর Thread নেই |
| সম্পর্কহীন Endpoint-ও একই সময়ে ধীর | Shared Pool, কোনো আলাদা ব্যবস্থা নেই |
প্রতিটা Pool-এ এগুলো নজরে রাখো: Active Thread, Queue-র দৈর্ঘ্য, Task-এর অপেক্ষার সময় (চলার সময় থেকে আলাদা করে) আর Rejection-এর সংখ্যা। আগাম সতর্কতা হলো অপেক্ষার সময়। ব্যবহারকারী অভিযোগ করার আগেই এটা বাড়ে।
সমাধান, যে ক্রমে করবে:
১. প্রতিটা বাইরের Call-এ Timeout। ধরে রাখার সময় এটাই ঠিক করে। না থাকলে একটা Dependency একটা Thread-কে চিরকাল আটকে রাখতে পারে। মান কীভাবে বাছবে, তা Chapter 28-এ।
২. সীমিত Queue আর দ্রুত ফিরিয়ে দেওয়া, ওপরে যেমন দেখেছ।
৩. Bulkhead। ঝুঁকির Dependency-কে তার নিজের ছোট Pool বা Concurrency Limit দাও, যাতে নষ্ট হওয়াটা এক কামরাতেই থাকে। Bulkhead আর Circuit Breaker আছে Chapter 28-এ।
৪. পুরো পথে Pool মিলিয়ে নাও। ২০০ Request Thread আর ২০ Database Connection মানে ১৮০ Thread Connection Pool-এ দাঁড়াতে পারে। ছোট ধাক্কায় এটা চলে, কিন্তু Connection পাওয়ার Timeout ছোট রাখো, যাতে ঝুলে না থেকে ব্যর্থ হয়।
৫. অন্য Concurrency Model ভাবো, যে কাজ বেশিরভাগ সময় শুধু অপেক্ষা করে তার জন্য: Event Loop বা Virtual Thread (Lesson ৩.৬)।
#যে Pool নিজের জন্যই অপেক্ষা করে
কোনো Lock চোখে না পড়লেও Pool Deadlock করতে পারে। Pool-এ চলা একটা Task একই Pool-এ একটা Child Task পাঠায়, আর সেটা শেষ না হওয়া পর্যন্ত আটকে থাকে। চাপ কম থাকলে Child-এর জন্য একটা Worker ফাঁকা পাওয়া যায়। চাপ বাড়লে প্রতিটা Worker-এর হাতে একটা করে Parent, সবাই Child-এর অপেক্ষায়, আর Child-রা Queue-তে বসে এমন Worker-এর জন্য, যে কোনোদিন ফাঁকা হবে না।
যে Pool নিজের জন্যই অপেক্ষা করে
একটা Task একই Pool-এ একটা Child Task পাঠায়, আর সেটা শেষ না হওয়া পর্যন্ত আটকে থাকে। সব Test পাশ করে, তারপর চাপ পড়লে জমে যায়।
পরিস্থিতি
২ Worker-এর Pool-এ একটা Parent Task।
একটা Parent চলে Worker ১-এ, Child পাঠিয়ে আটকে থাকে। Worker ২ ফাঁকা।
Test-এ এটা ধরা পড়ে না, কারণ Test সচরাচর Pool ভরায় না। Production-এ শূন্য CPU নিয়ে Service জমে যায়। সমাধান:
- Pool-এর Task-এ একই Pool-এর অন্য Task-এর জন্য আটকে থেকো না। বরং পরের ধাপটা জুড়ে দাও (
CompletableFuture-এthenCompose)। - নির্ভরতার প্রতিটা স্তরে আলাদা Pool রাখো, যাতে নির্ভরতা এক দিকেই যায়, চক্র হতে না পারে।
- Recursive কাজে Work-stealing Pool ব্যবহার করো, যেমন
ForkJoinPool, যেখানে অপেক্ষায় থাকা Worker Queue-র অন্য Task চালাতে পারে। get()-এর মতো Call-এ সবসময় Timeout দাও, যাতে জমে যাওয়াটা চোখে পড়ার মতো একটা Error হয়ে ওঠে।
Connection Pool-এও একই Bug আছে: প্রতিটা Request একটা Connection ধরে রেখে দ্বিতীয়টা চায়, যেমন আলাদা Connection-এ Nested Transaction খুলে। HikariCP-র Documentation নিরাপদ ন্যূনতম মাপটা দেয়: Pool = Thread × (Task প্রতি Connection − ১) + ১। ১০টা Thread-এর প্রতিটায় ২টা Connection লাগলে ১১টা। ১০টার Pool Timeout না হওয়া পর্যন্ত জমে থাকতে পারে।
#Pool-এর আরও কিছু রোজকার ফাঁদ
- Exception গায়েব হয়ে যায়।
submit-এ Exception থাকে ফেরত আসা Future-এর ভেতরে। Future কেউ না পড়লে Error-ও কেউ দেখে না। Task-এর ভেতরেই Log করো, নয়তো Future দেখো। - Thread-local Data এক Task থেকে আরেক Task-এ যায়। Thread বারবার ব্যবহার হয়, তাই এক Task যদি User ID বা Security Context রেখে যায়, পরের Task সেটা দেখতে পায়, আর এটা Security Bug হয়ে উঠতে পারে।
finally-তে সবসময় মুছে ফেলো। - Shutdown সাবধানে করো।
shutdown()ডাকো,awaitTerminationদিয়ে নির্দিষ্ট সময় অপেক্ষা করো, দরকার হলে তারপরshutdownNow()। SIGTERM পেলেই এটা করো (Lesson ৩.১), নইলে প্রতিটা Deploy-এ Queue-র Task হারাবে। - Thread-এর নাম দাও।
pool-3-thread-17ভরা Thread Dump প্রায় কিছুই বলে না;payment-client-17বলে দেয় কোথায় তাকাতে হবে। - Virtual Thread-এর Pool বানিয়ো না। এগুলো বানানোই হয়েছে সস্তা করে, তাই Concurrency সীমিত করো Semaphore দিয়ে (Lesson ৩.৬)।
#Deadlock
Deadlock হলো এমন অবস্থা, যেখানে দুই বা তার বেশি Thread বা Process প্রত্যেকে এমন Resource-এর জন্য অপেক্ষা করছে যা তাদেরই অন্য কারও হাতে, ফলে কেউই আর কখনো এগোতে পারে না। Note-এর ছোট সংস্করণে শুধু Process-এর কথা আছে, কিন্তু ধরে রেখে অপেক্ষা করতে পারে এমন সবকিছুতেই এটা খাটে: Thread, Process, Database Transaction, কিংবা একে অপরকে Call করা Service।
ধরো দুটো Thread আর দুটো Lock:
Thread A: Thread B:
acquire Lock 1 acquire Lock 2
wait for Lock 2 wait for Lock 1
এবার অবস্থাটা:
Thread A holds Lock 1, waits for Lock 2
Thread B holds Lock 2, waits for Lock 1
কেউই ছাড়বে না, তাই দুজনেই চিরকাল অপেক্ষা করবে। Deadlock-এর ঝামেলাটা এখানেই: এটা নির্ভর করে সময়ের ওপর। একই Code সাধারণত দিব্যি চলে, আর জমে যায় তখনই, যখন দ্বিতীয় Lock চাওয়ার আগেই দুজনে প্রথম Lock পেয়ে যায়।
দুটো Thread, দুটো Lock, একটা মারাত্মক সময়
A নেয় আগে Lock ১, পরে Lock ২। B নেয় আগে Lock ২, পরে Lock ১। Deadlock হবে কি না তা নির্ভর করে শুধু সময়ের ওপর, আর এজন্যই এটা সব Test পাশ করে যেতে পারে।
পরিস্থিতি
A, B-র চেয়ে একটু এগিয়ে থাকে। কোনো সমস্যা হয় না, অন্তত আজ।
কে কী ধরে আছে
| Thread | ধরে আছে | অপেক্ষায় |
|---|---|---|
| A | Lock 1 | - |
| B | - | - |
Thread A Lock ১ নিল। সেটা ফাঁকা ছিল, তাই সাথে সাথেই পেয়ে গেল।
#Coffman-এর চারটা শর্ত
১৯৭১ সালে Coffman, Elphick আর Shoshani চারটা শর্তের কথা বলেন, Deadlock থাকতে হলে যার সবগুলো একসাথে থাকতে হবে। এগুলো প্রয়োজনীয় শর্ত: যেকোনো একটা সরালেই Deadlock অসম্ভব।
| শর্ত | মানে | উদাহরণ | সরাবে কীভাবে |
|---|---|---|---|
| Mutual Exclusion | Resource এক সময়ে একটা Thread-ই ব্যবহার করতে পারে | Mutex Lock | ভাগ করে নাও: Read-only Data, Immutable Object, Lock-free Structure |
| Hold and Wait | Thread একটা Resource ধরে রেখে আরেকটার অপেক্ষা করে | Lock ১ হাতে, Lock ২-এর অপেক্ষা | সব একসাথে নাও, বা অপেক্ষার আগে ছেড়ে দাও |
| No Preemption | জোর করে Resource কেড়ে নেওয়া যায় না; ছাড়তে পারে শুধু যার হাতে আছে | OS কোনো Thread থেকে Mutex কেড়ে নেয় না | Timeout, Victim বন্ধ করা |
| Circular Wait | অপেক্ষারত Thread-রা একটা বন্ধ শিকল বানায় | A অপেক্ষায় B-র, B অপেক্ষায় A-র | সবার জন্য একটাই Lock-এর ক্রম |
Mutual Exclusion সরানো কঠিন, কারণ ভাগ করা গেলে তো Lock-ই লাগত না। বাস্তবে আক্রমণ করা হয় বাকি তিনটায়, সবচেয়ে বেশি Circular Wait-এ।
Note-এর তিন Thread-এর নকশায় ধরে রাখা আর অপেক্ষা গুলিয়ে গেছে, তাই পরিষ্কার করে আবার আঁকলাম:
Thread A holds R1, waits for R2 (held by B)
Thread B holds R2, waits for R3 (held by C)
Thread C holds R3, waits for R1 (held by A)
A → B → C → A the cycle
রিং-এ তিনটা Thread, আর Detector সেটা কীভাবে দেখে
A ধরে আছে R1, অপেক্ষা করছে R2-র; B ধরে আছে R2, অপেক্ষা করছে R3-র; C ধরে আছে R3, অপেক্ষা করছে R1-এর। তারপর দেখো একটা Chain, যা শুধু আটকে আছে বলে মনে হয়, Wait-for Graph, আর একজন Victim-কে মারলে কী হয়।
পরিস্থিতি
Deadlock শুধু দুই Thread-এ সীমিত না। চক্র যেকোনো লম্বা হতে পারে, আর তাই Code দেখে ধরা কঠিন।
কে কী ধরে আছে
| Thread | ধরে আছে | অপেক্ষায় |
|---|---|---|
| A | R1 | - |
| B | R2 | - |
| C | R3 | - |
প্রত্যেকে তার প্রথম Resource নিল: A পেল R1, B পেল R2, C পেল R3। সব ঠিকঠাক মনে হচ্ছে।
দুটো খুঁটিনাটি, যা Interviewer-রা পছন্দ করেন:
- লম্বা Wait Chain মানেই Deadlock না। A → B → C, আর C চলছে, তাহলে C শেষ করলে সব আপনাআপনি খুলে যায়। একটা ধীর Lock-ধারীর পেছনে অনেক Thread আটকে থাকলে Dashboard-এ একই রকম দেখায়, কিন্তু সমাধান আলাদা।
- প্রতিটা Resource একটাই হলে তবেই চক্র মানে নিশ্চিত Deadlock। একই রকম কয়েকটা থাকলে (৫ Connection-এর Pool) Resource Graph-এ চক্র থাকলেও সেটা খুলে যেতে পারে, কারণ অন্য কোনো ধারক একটা ছেড়ে দিতে পারে। তাই Pool-এ প্রশ্নটা হলো, “সবাই কি একসাথে অপেক্ষায় থাকতে পারে?” আগের অংশের সূত্র ঠিক সেটারই উত্তর দেয়।
#Deadlock সামলানোর চারটা কৌশল
মোটা দাগে চারটা পথ, আর বাস্তব System এগুলো মিলিয়ে ব্যবহার করে।
| কৌশল | ধারণা | কোথায় দেখবে |
|---|---|---|
| Prevention | এমনভাবে নকশা করো যাতে একটা শর্ত কখনো না থাকে | Application Code-এ Lock Ordering |
| Avoidance | চলার সময় প্রতিটা অনুরোধ যাচাই করে অনিরাপদগুলো ফেরাও (Banker's Algorithm) | প্রধানত বইয়ে, কারণ ভবিষ্যতের চাহিদা জানা লাগে |
| Detection আর Recovery | হতে দাও, চক্র খুঁজে বের করো, একজন Victim বন্ধ করো | Database |
| পাত্তা না দেওয়া | কদাচিৎ জমে যাওয়া মেনে নিয়ে Restart করো | User Program-এর জন্য অনেক OS; Process Restart করা Watchdog আর Health Check |
#বাস্তবে Prevention
| কৌশল | কোন শর্ত সরায় | দাম |
|---|---|---|
| Lock Ordering | Circular Wait | সবাইকে সব পথে মানতে হবে |
| সব Lock একসাথে নেওয়া | Hold and Wait | সব Lock আগে জানতে হয়; Concurrency কমে |
| Lock-এর Scope ছোট রাখা | Hold and Wait কমায় | কয়েক ধাপের Update হয়তো আর Atomic থাকবে না |
| অকারণ Nested Lock এড়ানো | Hold and Wait | কখনো কখনো নকশা বদলাতে হয় |
| Timeout আর Back Off | কার্যত Hold and Wait, No Preemption | এটা Recovery, Prevention না: চক্র তৈরি হতে পারে, তারপর খুলে যায় |
| Lock Hierarchy | Circular Wait | স্তর ধরে ক্রম, বড় Codebase-এর জন্য |
সবচেয়ে কাজের হলো Lock Ordering:
Always:
Lock 1 → Lock 2
কেউ আগে Lock ২ নিয়ে পরে Lock ১ নেবে না। এতে প্রতিটা অপেক্ষা হয় ছোট Lock থেকে বড় Lock-এর দিকে, আর শুধু ওপরের দিকে যাওয়া তীর দিয়ে চক্র বন্ধ করা যায় না।
ক্রমটা যখন Data থেকে আসে, তখন সাজিয়ে নাও। transfer(from, to) যদি আগে from, পরে to Lock করে, তাহলে ৭→৯ আর ৯→৭ একসাথে চললে Deadlock হয়, কারণ দুই Call দুই উল্টো ক্রম নেয়। টাকা যেদিকেই যাক, যে Account-এর ID ছোট সেটা আগে Lock করো। Database-এর Row-তেও তাই।
Ordering এমন জায়গায় ভাঙতে পারে, যেটা তুমি পড়োই না। Lock হাতে রেখে Callback, Listener বা Library Call করলে সেই Code যেকোনো ক্রমে অন্য Lock নিতে পারে। যে Code তোমার হাতে নেই, Lock ধরে সেটা Call কোরো না, আর Network Call-এর সময় তো কখনোই Lock ধরে রেখো না: ধীর একটা Dependency তাতে আটকে থাকা Thread-এর লাইনে পরিণত হয়, আর দুটো Service একে অপরের সাথে এটা করলে Machine পেরিয়ে চক্র তৈরি হতে পারে।
Deadlock এড়ানোর চারটা পথ, আর একটা ফাঁদ
প্রতিটা সমাধান চারটা শর্তের একটা সরিয়ে দেয়। সেই একই দুই Thread, একই দুই Lock।
পরিস্থিতি
সবাই Lock ২-এর আগে Lock ১ নেয়। এতে Circular Wait থাকে না।
A নিল Lock ১, ঠিক করা ক্রমের প্রথম Lock।
এর Code-এর খুঁটিনাটি (synchronized, ReentrantLock.tryLock, Go আর Python) আছে Chapter 14-এ।
#Deadlock খুঁজে বের করা
মূল ধারণা হলো Wait-for Graph। প্রতিটা Thread-এর জন্য একটা Node, আর যে অপেক্ষা করছে তার থেকে যে ধরে আছে তার দিকে একটা তীর। চক্র মানেই Deadlock। Database আর JVM তাদের Lock Table থেকে এই Graph বানায়।
চলন্ত Process-এ Thread Dump নাও:
| Runtime | Command | কী পাবে |
|---|---|---|
| Java (JDK-র Tool) | jstack <pid>, jcmd <pid> Thread.print বা kill -3 <pid> | সব Stack, আর চক্র থাকলে “Found one Java-level deadlock” অংশ |
| Go | kill -QUIT <pid> | সব Goroutine-এর Stack। Runtime নিজে Deadlock জানায় শুধু সব Goroutine ঘুমিয়ে থাকলে |
| Python | py-spy dump --pid <pid>, বা faulthandler | সব Thread-এর Stack |
| Native Code | gdb -p <pid>, তারপর thread apply all bt | সব Thread-এর Stack; চক্র হাতে খুঁজতে হয় |
কয়েক সেকেন্ড পরপর দুই-তিনটা Dump নাও। সবগুলোতে যে Thread একই জায়গায়, সে সত্যিই আটকে। অনেক Thread এক Frame-এ, আর একটা নড়ছে? তাহলে সেটা ধীর Holder, Deadlock না।
Database-এ Engine সাধারণত নিজেই খুঁজে নেয়:
-- PostgreSQL: who is blocked, and by whom, right now
SELECT pid, pg_blocking_pids(pid) AS blocked_by, wait_event_type, state, query
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;
-- The locks themselves
SELECT locktype, relation::regclass, mode, granted, pid
FROM pg_locks
WHERE NOT granted;
MySQL-এ SHOW ENGINE INNODB STATUS-এর LATEST DETECTED DEADLOCK অংশে দুই Transaction-ই দেখা যায়।
| PostgreSQL | MySQL InnoDB | |
|---|---|---|
| কখন যাচাই করে | Wait deadlock_timeout পেরোলে (ডিফল্ট ১ s) | Transaction-কে অপেক্ষা করতে হবে, ঠিক তখনই |
| কোনটা বন্ধ হয় | আগে বলা কঠিন; ভরসা কোরো না | যেটাকে সবচেয়ে ছোট ধরে (কত Row বদলেছে) |
| Error | 40P01 deadlock detected | 1213, SQLSTATE 40001 |
| সাধারণ Lock Timeout | lock_timeout, ডিফল্টে বন্ধ | innodb_lock_wait_timeout, ডিফল্ট ৫০ s, Error 1205; ডিফল্টে শুধু Statement Rollback হয় |
Database চক্র খুঁজে বের করে Victim বাছে
দুটো Transaction একই দুটো Row উল্টো ক্রমে Update করে। Thread-এর সাথে পার্থক্য হলো, Transaction নিরাপদে Rollback করা যায়, তাই Database নিজেই সামলে নেয়, আর তোমার Code-কে আবার চেষ্টা করতে হয়।
পরিস্থিতি
PostgreSQL আগে অপেক্ষা করে, আর Lock Wait deadlock_timeout-এর চেয়ে লম্বা হলে তবেই চক্র খোঁজে।
Update একটা Row Lock নেয়, আর Commit বা Rollback পর্যন্ত ধরে রাখে। মানে প্রতিটা Statement-এ Transaction-এর হাতে জিনিস বাড়ে।
Deadlock Error মানে Database-এর Bug না। এটা স্বাভাবিক আচরণ, আর তোমার Code-কেই এটা সামলাতে হবে: Error ধরো, অল্প এলোমেলো সময় অপেক্ষা করো, আর পুরো Transaction আবার চালাও। তারপর ঘটার হার কমাও: Row-এর ক্রম এক রাখো, Transaction ছোট রাখো, Transaction-এর ভেতরে User-এর অপেক্ষা বা Remote Call রেখো না, আর Index দাও যাতে Update শুধু দরকারি Row-ই Lock করে। Deadlock Log করো (PostgreSQL-এ log_lock_waits), আর হার বাড়লে সেটাকে Bug Report হিসেবে নাও। Transaction নিয়ে আরও আছে Chapter 22-এ।
Machine পেরোলে পুরো Graph কেউই দেখতে পায় না। Transaction ১ যদি Shard A-তে একটা Row ধরে Shard B-তে অপেক্ষা করে, আর Transaction ২ করে উল্টোটা, প্রতিটা Database দেখে শুধু একটা সাধারণ Lock Wait। Service A, B-কে Call করছে আর B, A-কে, প্রত্যেকে অন্যের Pool শেষ করছে, এখানেও তাই। বাস্তব সমাধান হলো সব জায়গায় Timeout, সাথে Access-এর একই ক্রম।
#Deadlock, Starvation আর Livelock
| Deadlock | Starvation | Livelock | |
|---|---|---|---|
| কী হয় | জড়িত সবাই আটকে | একটা Thread অনেকক্ষণ সুযোগ পায় না | সবাই সক্রিয়, কিন্তু কাজ এগোয় না |
| CPU | ওই Thread-গুলো সাধারণত খায় না | বাকিরা স্বাভাবিক চলে | বেশি: Thread চলতেই থাকে |
| System এগোয়? | ওই Thread-গুলো না | হ্যাঁ, Victim ছাড়া | না |
| নিজে থেকে শেষ হয়? | কখনো না | হতে পারে, চাপ কমলে | হতে পারে, সময়ের ভাগ্যে |
| সমাধান | Lock আর Resource-এর নকশা | Fair Scheduling, Aging | এলোমেলো Backoff, Tie-breaker |
| কী দেখবে | জমে আছে, CPU কম, প্রতি Dump-এ একই Stack | এক ধরনের কাজের Latency বিশাল, বাকি সব ঠিক | CPU ব্যস্ত, Throughput শূন্য, Stack বদলাচ্ছে |
Livelock: সংঘাত এড়াতে Thread-রা একে অপরের কাণ্ডে বারবার সাড়া দেয়, অথচ আসল কাজ এগোয় না। প্রায়ই এর জন্ম হয় Deadlock সারানোর ভালো উদ্দেশ্য থেকে, মানে দেরি ছাড়াই tryLock আর Retry। ওষুধ হলো এলোমেলো দেরি, যাতে একজন এগিয়ে যায়।
Livelock: সবাই ভদ্র, কাজ কিছুই হয় না
দুই Thread-ই সংঘাত দেখলে পিছিয়ে যায়। একই তালে করতে থাকলে CPU ব্যস্ত, কাজ কিছুই হয় না, যতক্ষণ না এলোমেলো দেরি সমতাটা ভাঙে।
পরিস্থিতি
প্রত্যেকে অপেক্ষা ছাড়াই দ্বিতীয় Lock চেষ্টা করে, না পেলে ছেড়ে দিয়ে সাথে সাথে আবার চেষ্টা করে।
রাউন্ড ১। A নিল Lock ১, B নিল Lock ২।
Starvation: একটা Thread বারবার হেরে যায়, সাধারণত নিচু Priority-র কেউ, যে CPU বা Resource পায় না। কারণ শুধু Priority না। Read-Write Lock-এ Reader-দের স্রোত Writer-কে আটকে রাখতে পারে, আর Unfair Lock তাকেই সুবিধা দিতে পারে যে এখন চলছে। Fair Lock আর Queue এটা সারায়, Throughput-এ কিছুটা দাম দিয়ে। Aging সময়ের সাথে অপেক্ষারত Task-এর Priority বাড়িয়ে দেয়।
#Priority Inversion
Priority Inversion মানে Lock-এর কারণে উঁচু Priority-র Task আটকে আছে নিচু Priority-র Task-এর পেছনে। Note-এর সংজ্ঞা থেমে যায় “Low-এর হাতে সেই Lock যা High চায়”, এখানে। এটুকু ছোট অপেক্ষা স্বাভাবিক। বিপজ্জনক রূপে লাগে তৃতীয় একজন: Medium, যার Lock লাগে না, সে Low-কে সরিয়ে দেয়। এবার Low শেষ করতে পারে না, তাই High চলতে পারে না, যতক্ষণ Medium চলে ততক্ষণ। কার্যত Medium, High-এর ওপরে উঠে গেল।
বিখ্যাত ঘটনাটা ১৯৯৭ সালের Mars Pathfinder। নামার কিছুদিন পরেই Lander-এর Computer বারবার Reset হতে থাকে। নিচু Priority-র আবহাওয়ার Task একটা Shared Bus-এর Mutex ধরে ছিল, উঁচু Priority-র Bus Task তার অপেক্ষায়, আর মাঝারি Priority-র Communication Task, Low-কে চলতেই দিচ্ছিল না। Watchdog দেখল উঁচু Priority-র Task Deadline মিস করেছে, আর পুরো System Reset করে দিল। Engineer-রা মাটিতে থাকা একটা কপিতে সমস্যাটা আবার ঘটিয়ে দেখলেন, তারপর ছোট একটা পরিবর্তন পাঠালেন, যা VxWorks-এ ওই Mutex-এর Priority Inheritance চালু করে, আর Reset থেমে গেল।
Priority Inheritance মানে, High যে Lock চায় সেটা Low ধরে থাকলে, ততক্ষণ Low চলে High-এর Priority নিয়ে। Linux-এ Real-time কাজের জন্য PTHREAD_PRIO_INHERIT দিয়ে এটা পাওয়া যায়।
Starvation আর Priority Inversion
আলাদা Priority-র তিনটা Task, আর একটা Shared Lock। এক ক্ষেত্রে নিচু Task কখনো চলেই না। অন্য ক্ষেত্রে, সেই নিচু Task-ই সবচেয়ে জরুরি Task-কে আটকে দেয়।
পরিস্থিতি
পুরো System এগোয়, শুধু একটা Task কখনো পালা পায় না।
High Lock নেয়, কাজ করে, ছেড়ে দেয়।
Real-time Code তুমি হয়তো কোনোদিন লিখবে না, কিন্তু এই ছাঁচটা সব জায়গায়। রাতের Batch Job কম CPU Priority নিয়ে ব্যস্ত Table-এ Row Lock ধরে আছে, আর কাস্টমার-এর Checkout তার পেছনে অপেক্ষায়। Shared Lock-এর ভেতরে থাকা নিচু Priority-র Task তার Container-এর CPU Limit-এ Throttle হলো, আর ওই Lock লাগে এমন সবকিছু থমকে গেল। নিয়ম একটাই: জরুরি কাজের দরকারি জিনিস নিচু Priority-র কাজকে বেশিক্ষণ ধরে রাখতে দিয়ো না।
#System Design-এ এর মানে কী
- প্রতিটা Pool একইসাথে একটা Concurrency Limit আর একটা Queue। Diagram-এ এদের নাম লেখো: Request Thread, Database Connection, Outbound Client। Overload দেখা দেয় সবচেয়ে ছোটটায়।
- সংখ্যা ধরে মাপো। আসার হার × প্রতি Request-এর সময় বলে কত Thread লাগবে; Core × (১ + অপেক্ষা ÷ হিসাব) বলে CPU কতগুলো কাজে লাগাতে পারবে। প্রথমটা দ্বিতীয়টার চেয়ে অনেক বড় হলে তোমার দরকার আরও Machine বা অন্য Model, বড় Pool না।
- সবকিছুর সীমা দাও। সীমিত Queue, Timeout আর Rejection মিলে Overload-কে সামলানো যায় এমন, চোখে পড়ার মতো ঘটনা বানায়।
- আলাদা করে রাখো। একটা ধীর Dependency যেন সব Thread নিতে না পারে।
- ধরে নাও Database Deadlock হবেই। Row-এর ক্রম ঠিক রাখো, আর Retry সবসময় লেখো।
- ধীর কাজের সময় Lock ধরে রেখো না, Network Call-এর সময় তো একদমই না।
- Distributed Lock-এও Deadlock হয়। Lease বা Expiry হলো সেখানে Preemption যোগ করার উপায়। এগুলো Chapter 38-এ।
#Interview-তে যেভাবে আসে
| প্রশ্ন | ভালো উত্তরে যা থাকবে |
|---|---|
| “এই Thread Pool-এর মাপ কীভাবে ঠিক করবে?” | CPU আর I/O-র সূত্র, তারপর পরের ধাপের সীমা, Little-এর সূত্র, Load Test |
| “সব Request ধীর, কিন্তু CPU ১০%। কী হচ্ছে?” | Thread অপেক্ষা করছে: ফুরোনো Pool, ধীর Dependency, Lock-এর ঠেলাঠেলি; Thread Dump আর Pool Metric দেখো |
| “Deadlock কী? কীভাবে ঠেকাবে?” | চার শর্ত, তারপর Lock Ordering; Timeout হলো Recovery; Data থেকে ক্রম এলে ID সাজাও |
| “দুটো Money Transfer Deadlock করছে। ঠিক করো।” | ID-র ক্রমে Account Lock করো; SQL-এ ID-র ক্রমে Row Update, আর 40P01 বা 1213-এ Retry |
| “Queue ভরে গেলে কী হবে?” | Rejection Policy বাছাই; Retry-After-সহ 503; সীমাহীন Queue কখনো না |
| “Deadlock না Livelock?” | Deadlock: CPU কম, Stack জমে আছে। Livelock: CPU বেশি, Throughput নেই, Stack বদলাচ্ছে |
| “একটা ধীর Service অন্যদের ক্ষতি করা কীভাবে থামাবে?” | Timeout, Bulkhead, Circuit Breaker (Chapter 28) |
#Try it yourself
১. Thread-এর সংখ্যা Throughput কীভাবে বদলায় দেখো। এটা Save করে চালাও। ২০০টা Task প্রতিটা ৫০ ms অপেক্ষা করে, ঠিক Database Call-এর মতো:
import time
from concurrent.futures import ThreadPoolExecutor
def io_task(_):
time.sleep(0.05) # 50 ms of waiting
for workers in (1, 4, 20, 100):
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=workers) as pool:
list(pool.map(io_task, range(200)))
print(f"{workers:>3} workers: {time.perf_counter() - start:5.2f} s")
মোটামুটি ১০ s, ২.৫ s, ০.৫ s আর ০.১ s দেখবে। এই Task-গুলো শুধু অপেক্ষাই করে, তাই Worker বাড়লে লাভ হয়। time.sleep-এর জায়গায় CPU খাটানো Loop দিলে সেই লাভটা উধাও হয়ে যাবে।
২. সত্যিকারের Deadlock বানিয়ে Stack পড়ো। Thread-গুলো Daemon, তাই Program তবু বের হতে পারবে:
import faulthandler
import threading
import time
lock1 = threading.Lock()
lock2 = threading.Lock()
def worker_a():
with lock1:
time.sleep(0.1) # give B time to take lock2
with lock2:
print("A finished")
def worker_b():
with lock2: # swap these two lines to apply lock ordering
time.sleep(0.1)
with lock1:
print("B finished")
a = threading.Thread(target=worker_a, name="A", daemon=True)
b = threading.Thread(target=worker_b, name="B", daemon=True)
a.start()
b.start()
time.sleep(1)
print("alive after 1 s:", a.is_alive(), b.is_alive())
faulthandler.dump_traceback(all_threads=True)
Dump দেখাবে প্রতিটা Thread তার দ্বিতীয় Lock-এর লাইনে আটকে। এবার worker_b-কে দিয়ে আগে lock1 নেওয়াও, আবার চালাও, দেখো দুজনেই শেষ করছে।
৩. Lock ছাড়াই একটা Pool-কে Deadlock করাও:
import os
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import TimeoutError as FutureTimeout
pool = ThreadPoolExecutor(max_workers=2)
def child():
return "child done"
def parent():
return pool.submit(child).result() # blocks a worker until child runs
parents = [pool.submit(parent) for _ in range(2)]
try:
print([p.result(timeout=2) for p in parents], flush=True)
except FutureTimeout:
print("stuck: both workers wait for children that are still queued", flush=True)
os._exit(0) # a stuck pool would block a normal exit
range(2) বদলে range(1) করো, কাজ করবে। এভাবেই এই Bug Test পেরিয়ে যায়।
৪. PostgreSQL-কে দিয়ে Victim বাছাও। দুটো psql Session খোলো:
CREATE TABLE accounts (id int PRIMARY KEY, balance int);
INSERT INTO accounts VALUES (1, 100), (2, 100);
-- Session 1:
BEGIN;
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
-- Session 2:
BEGIN;
UPDATE accounts SET balance = balance - 10 WHERE id = 2;
-- Session 1 (this one waits):
UPDATE accounts SET balance = balance + 10 WHERE id = 2;
-- Session 2 (after about a second, one session gets the error):
UPDATE accounts SET balance = balance + 10 WHERE id = 1;
-- ERROR: deadlock detected
Session ১ অপেক্ষায় থাকার সময় তৃতীয় একটা Session-এ ওপরের pg_blocking_pids Query-টা চালাও।
৫. তোমার চালানো যেকোনো Java Service-এর Thread Dump নাও: jcmd <pid> Thread.print। গুনে দেখো কোন অবস্থায় কয়টা Thread, আর বেশিরভাগ কোথায় অপেক্ষা করছে।
#Summary
মূল শিক্ষা
- Thread Pool হলো একটা Queue আর নির্দিষ্ট কিছু বারবার ব্যবহৃত Worker। এটা Concurrency সীমিত রাখে, আর Overload-কে Queue-র অপেক্ষায় বদলে দেয়।
- CPU-bound: Thread ≈ Core। I/O-bound: Thread ≈ Core × (১ + অপেক্ষা ÷ হিসাব)। তারপর পরের ধাপের সরু Resource দেখো, আর Load Test করো।
- কিছু ফাঁকা থাকলে তবেই বাড়তি Thread কাজে লাগে। তার পরে শুধু Latency বাড়ে।
- সীমিত Queue আর ভেবেচিন্তে বাছা Rejection Policy রাখো। Java-র
ThreadPoolExecutorবাড়তি Thread চালুর আগে Queue ভরে। - Pool Exhaustion দেখতে এমন: Latency বেশি, CPU কম। Timeout ধরে রাখার সময়ে ছাদ বসায়, Bulkhead ক্ষতি আটকে রাখে।
- Pool-এর Task একই Pool-এর Task-এর জন্য আটকে থাকলে Lock ছাড়াই Deadlock হতে পারে।
- Deadlock-এ Coffman-এর চারটা শর্তই লাগে: Mutual Exclusion, Hold and Wait, No Preemption, Circular Wait। যেকোনো একটা ভাঙো।
- প্রধান Prevention হলো Lock Ordering; ক্রম Data থেকে এলে ID দিয়ে সাজাও। Timeout হলো Recovery, Prevention না।
- Wait Chain মানেই Deadlock না; শুধু চক্রই Deadlock। চক্র খোঁজো Wait-for Graph, Thread Dump আর
pg_locksদিয়ে। - Database Victim বন্ধ করে, তাই তোমার Code-কে পুরো Transaction আবার চালাতে হবে। Machine পেরোলে চক্র কেউ ধরে না, তাই Timeout রাখো।
- Deadlock: জমে আছে, CPU কম। Livelock: ব্যস্ত, কাজ এগোয় না। Starvation: বাকিরা এগোয়, একজন হারে। Priority Inversion: Low-এর হাতের Lock-এর সুবাদে Medium, High-কে দেরি করায়।
আগে নিজে উত্তর দাও, তারপর ব্যাখ্যাটা পড়ো। ঠিক হলেও পড়ো, কারণ ভুল Option-গুলো কেন ভুল, সেটাও ওখানে আছে।
নিজেকে যাচাই করোপ্রশ্ন
প্রতিটা Task-এর জন্য নতুন Thread না বানিয়ে Thread Pool ব্যবহারের মূল কারণ কী?
নিজেকে যাচাই করোপ্রশ্ন
Pool-এ ২০টা Worker, প্রতিটা Task-এ ৫০ ms লাগে। একসাথে ১০,০০০ Task এল। শেষটা শেষ হতে মোটামুটি কত সময় লাগবে?
নিজেকে যাচাই করোপ্রশ্ন
একটা Service শুধু Image Resize করে, পুরোটাই CPU-র কাজ, Machine-এ ৮টা Core। Pool-এর মাপ কত দিয়ে শুরু করা উচিত?
নিজেকে যাচাই করোপ্রশ্ন
৪ Core-এর Server-এ প্রতিটা Request ২০ ms হিসাব করে, ১৮০ ms I/O-র অপেক্ষা করে। পুরো CPU ব্যবহার ধরলে সূত্র কত দেয়?
নিজেকে যাচাই করোপ্রশ্ন
৪০ Thread-এর Pool ১০টা Connection দিয়ে Database Call করে, প্রতিটা Query একটা Connection ১০০ ms ধরে রাখে। Thread যতই হোক, সেকেন্ডে কত Query-র বেশি হবে না?
নিজেকে যাচাই করোপ্রশ্ন
Incident চলাকালে Dashboard দেখাচ্ছে CPU ৮%, p99 Latency ৪০ s, আর Active Thread সর্বোচ্চে আটকে। সম্ভাব্য কারণ কী?
নিজেকে যাচাই করোপ্রশ্ন
Java-তে একটা Pool: Core ১০, Maximum ৫০, Queue-র জায়গা ১০,০০০। Traffic ভারী, কিন্তু Queue-তে কখনো ১০,০০০-এর নিচেই থাকে। Thread চলবে কয়টা?
নিজেকে যাচাই করোপ্রশ্ন
টানা Overload-এ থাকা Server-এ Executors.newFixedThreadPool(20)-এর প্রধান বিপদ কী?
নিজেকে যাচাই করোপ্রশ্ন
ধীর Task-এর ধাক্কায় Executors.newCachedThreadPool-এর বিশেষ ঝুঁকি কোনটা?
নিজেকে যাচাই করোপ্রশ্ন
একটা API-র Request Pool ভরা। ব্যবহারকারী আর System, দুজনের জন্যই সাধারণত কোন উত্তরটা সবচেয়ে ভালো?
নিজেকে যাচাই করোপ্রশ্ন
CallerRunsPolicy নিয়ে কোন দুটো কথা ঠিক?
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
একটা App সেকেন্ডে ২০০ Task সামলায়, আর ব্যবহারকারী এক সেকেন্ডে হাল ছেড়ে দেয়। তুমি ঠিক করলে সর্বোচ্চ ০.৫ s অপেক্ষা চলবে। Queue কত লম্বা হওয়া উচিত?
নিজেকে যাচাই করোপ্রশ্ন
একটা Service-এ ২০০ Thread-এর একটাই Shared Pool। Checkout সেকেন্ডে ৫০ বার এমন Payment Provider-কে Call করে, যা ঝুলে আছে, আর Client Timeout ৩০ s। /search-ও কেন পড়ে যায়?
নিজেকে যাচাই করোপ্রশ্ন
একই Service-এ Payment-এর সমস্যার সময় তুমি Search-কে সুস্থ রাখতে চাও। কোন দুটো পরিবর্তন সরাসরি সবচেয়ে বেশি কাজে দেবে?
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
চাপে একটা Service জমে গেল, CPU ০। Dump দেখাচ্ছে Pool-এর সব Worker future.get()-এ আটকে, আর Queue-তে আছে ওই Worker-দেরই পাঠানো Task। এটা কী?
নিজেকে যাচাই করোপ্রশ্ন
প্রতিটা Task-এ একসাথে ২টা Database Connection লাগে, আর একসাথে ১০টা Thread Task চালায়। HikariCP-র সূত্রে সবচেয়ে ছোট কোন Pool জমে যাবে না?
নিজেকে যাচাই করোপ্রশ্ন
Thread Pool-এ Thread-local Data কেন Security-র সমস্যা হতে পারে?
নিজেকে যাচাই করোপ্রশ্ন
কোনটা Coffman-এর চার শর্তের একটা NOT?
নিজেকে যাচাই করোপ্রশ্ন
Thread A ধরে আছে R1, অপেক্ষায় R2-র। Thread B ধরে আছে R2, অপেক্ষায় R3-র। Thread C ধরে আছে R3, আর স্বাভাবিক চলছে। এটা কি Deadlock?
নিজেকে যাচাই করোপ্রশ্ন
transfer(from, to) আগে from, পরে to Lock করে। transfer(7, 9) আর transfer(9, 7) একসাথে চললে Deadlock হচ্ছে। সবচেয়ে ভালো সমাধান কী?
নিজেকে যাচাই করোপ্রশ্ন
সবার জন্য একটাই Lock-এর ক্রম করলে Coffman-এর কোন শর্তটা চলে যায়?
নিজেকে যাচাই করোপ্রশ্ন
একটা Team বলছে ১০০ ms Timeout-এর tryLock তাদের Deadlock ঠেকায়। সবচেয়ে সঠিক উত্তর কোনটা?
নিজেকে যাচাই করোপ্রশ্ন
একটা Team কড়াভাবে Lock-এর ক্রম মানে, তবু Thread Dump-এ Deadlock। সম্ভাব্য কোন দুটো কারণ?
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
PostgreSQL তোমার Transaction-এ Error 40P01 দিল। Application কী করবে?
নিজেকে যাচাই করোপ্রশ্ন
চক্র খোঁজার আগে PostgreSQL কেন deadlock_timeout, ডিফল্ট ১ s, অপেক্ষা করে?
নিজেকে যাচাই করোপ্রশ্ন
দুটো Transaction দুটো আলাদা Database Shard জুড়ে Deadlock করল। কোনো Database কেন Deadlock জানায় না?
নিজেকে যাচাই করোপ্রশ্ন
CPU ১০০%, Throughput শূন্য, আর কয়েক সেকেন্ড পরপর নেওয়া Thread Dump-এ Thread-রা ভিন্ন ভিন্ন জায়গায়, বারবার Lock নিচ্ছে আর ছাড়ছে। এটা কী?
নিজেকে যাচাই করোপ্রশ্ন
একটা ধীর Lock-ধারীর পেছনে লম্বা অপেক্ষা থেকে Deadlock-কে আলাদা করে কোন দুটো লক্ষণ?
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
উঁচু Priority-র Task নিচু Priority-র Task-এর হাতের Lock-এর জন্য অপেক্ষায়, আর মাঝারি Priority-র একটা Task, যার Lock লাগে না, চলতেই আছে। প্রচলিত সমাধান কী?
নিজেকে যাচাই করোপ্রশ্ন
Read-Write Lock-এ একটা Writer কয়েক মিনিট ধরে অপেক্ষায়, কারণ Reader-দের অবিরাম স্রোত Lock ধরে রাখছে। Reader-রা স্বাভাবিক এগোচ্ছে। এটা কী, আর কীসে কাজ হবে?