Course-এর সূচি

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 শেষ করে।

Worker
Queue
Queue-এ0শেষ0
কাজ করছেI/O-তে আটকেঅপেক্ষায়ফাঁকা
ধাপ 1 / 5
1

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-এর ওপর চাপ
মেশানো কাজে একটাই SettingCPU-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 তুমি আগেই কোথায় দেখেছ

SystemPoolসাধারণ Setting
TomcatRequest ThreadmaxThreads, ডিফল্ট ২০০
PHP-FPMWorker Process (Process-এর Pool)pm.max_children, তুমি ঠিক করো
GunicornWorker Process, চাইলে Thread-সহ--workers, --threads
Python ThreadPoolExecutorWorker Threadডিফল্ট min(32, cores + 4)
Node.js libuvFile আর DNS কাজের Workerডিফল্টে ৪টা (Lesson ৩.৬)
Java ForkJoinPool Common PoolParallel Stream-এর Work-stealing WorkerCore − ১
HikariCP, PgBouncerDatabase 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 অপেক্ষা করে।

ThreadThroughputCPUকী ঘটছে
৪৪ ÷ ০.১ 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।

CPU Core
Thread
সূত্রCore = ৪কাজ৪ Core-এর সমান
Core-এ চলছেতৈরি, Core ফাঁকা নেইI/O-র অপেক্ষায়ফাঁকা
ধাপ 1 / 1
1

চার 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 হারায়
DiscardOldestPolicyQueue-র সবচেয়ে পুরনোটা ফেলে দেয়সর্বশেষ মানই যেখানে কাজের, পুরনোটা মূল্যহীনচুপচাপ Data হারায়, জরুরি কিছুও যেতে পারে

সীমিত Queue, আর সেটা ভরে গেলে কী হয়

একটা Java ThreadPoolExecutor: ২টা Core Thread, সর্বোচ্চ ৪টা, Queue-তে ৪টা জায়গা। এটা যে ক্রমে বড় হয় তা বেশিরভাগ মানুষকে অবাক করে, আর Rejection Policy ঠিক করে Overload-এর ধাক্কা কার গায়ে লাগবে।

পরিস্থিতি

নয়টা ধীর Task একটার পর একটা আসে।

Worker
Queue
এসেছে2
কাজ করছেQueue-তে✕ফেরতফাঁকা বা চালু হয়নি
ধাপ 1 / 5
1

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-এর ব্যবহারকারীরা।

ব্যবহারকারী
API২০০ Threadব্যস্ত ৫৫
Paymentবাইরের Service
২০০-এর মধ্যে ৫৫ Thread ব্যস্ত
ধাপ 1 / 5
1

স্বাভাবিক দিন: 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।

Worker
Queue
কাজ করছেআটকে, Child-এর অপেক্ষায়Queue-তেফাঁকা
ধাপ 1 / 2
1

একটা 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 ১
Lock ২
Thread B
নিল

কে কী ধরে আছে

Threadধরে আছেঅপেক্ষায়
ALock 1-
B--
ধাপ 1 / 4
1

Thread A Lock ১ নিল। সেটা ফাঁকা ছিল, তাই সাথে সাথেই পেয়ে গেল।

#Coffman-এর চারটা শর্ত

১৯৭১ সালে Coffman, Elphick আর Shoshani চারটা শর্তের কথা বলেন, Deadlock থাকতে হলে যার সবগুলো একসাথে থাকতে হবে। এগুলো প্রয়োজনীয় শর্ত: যেকোনো একটা সরালেই Deadlock অসম্ভব।

শর্তমানেউদাহরণসরাবে কীভাবে
Mutual ExclusionResource এক সময়ে একটা Thread-ই ব্যবহার করতে পারেMutex Lockভাগ করে নাও: Read-only Data, Immutable Object, Lock-free Structure
Hold and WaitThread একটা 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
R2
Thread B
R1
Thread C
R3

কে কী ধরে আছে

Threadধরে আছেঅপেক্ষায়
AR1-
BR2-
CR3-
ধাপ 1 / 4
1

প্রত্যেকে তার প্রথম 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 OrderingCircular 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 HierarchyCircular 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 থাকে না।

Thread A
Lock ১মালিক A
Lock ২
Thread B
নিল
ধাপ 1 / 4
1

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 নাও:

RuntimeCommandকী পাবে
Java (JDK-র Tool)jstack <pid>, jcmd <pid> Thread.print বা kill -3 <pid>সব Stack, আর চক্র থাকলে “Found one Java-level deadlock” অংশ
Gokill -QUIT <pid>সব Goroutine-এর Stack। Runtime নিজে Deadlock জানায় শুধু সব Goroutine ঘুমিয়ে থাকলে
Pythonpy-spy dump --pid <pid>, বা faulthandlerসব Thread-এর Stack
Native Codegdb -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-ই দেখা যায়।

PostgreSQLMySQL InnoDB
কখন যাচাই করেWait deadlock_timeout পেরোলে (ডিফল্ট ১ s)Transaction-কে অপেক্ষা করতে হবে, ঠিক তখনই
কোনটা বন্ধ হয়আগে বলা কঠিন; ভরসা কোরো নাযেটাকে সবচেয়ে ছোট ধরে (কত Row বদলেছে)
Error40P01 deadlock detected1213, SQLSTATE 40001
সাধারণ Lock Timeoutlock_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-এর চেয়ে লম্বা হলে তবেই চক্র খোঁজে।

Transaction ১Row ১ ধরে আছে
Database
Transaction ২
Row ১ Update
ধাপ 1 / 8
1

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

DeadlockStarvationLivelock
কী হয়জড়িত সবাই আটকেএকটা 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 চেষ্টা করে, না পেলে ছেড়ে দিয়ে সাথে সাথে আবার চেষ্টা করে।

Thread A
Lock ১মালিক A
Lock ২মালিক B
Thread B
নিল
ধাপ 1 / 5
1

রাউন্ড ১। 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 কখনো পালা পায় না।

HighPriority
MediumPriority
LowPriority
Lock
নিল
ধাপ 1 / 5
1

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-কে দেরি করায়।