3.2
Concurrency বনাম Parallelism, আর Context Switching
পালা করে কাজ বনাম একসঙ্গে কাজ, আর thread বদলানোর লুকানো দাম।
35 মিনিট পড়া + Animation
টিপস: ডটেড-আন্ডারলাইন করা যেকোনো শব্দে ট্যাপ বা Hover করলে তার অর্থ দেখবে।
এই Lesson শেষে তুমি পারবে
- Concurrency আর Parallelism ঠিকঠাক সংজ্ঞায় বলতে, Timeline-এ এঁকে দেখাতে, আর “Concurrency মানে এক Core” এই প্রচলিত ভুলটা ধরিয়ে দিতে।
- হিসাব করে বোঝাতে, I/O-নির্ভর কাজে Concurrency কেন Server-এর Throughput কয়েক গুণ বাড়ায়, অথচ এক Core-এ পুরো CPU-র কাজে কিছুই করে না।
- একটা Context Switch ধাপে ধাপে বলতে, Context-এ কী কী থাকে তা জানাতে, আর কীসে Switch হয় আর কীসে হয় না তা আলাদা করতে।
- Switch-এর সরাসরি খরচ (প্রায় ১–৫ µs) আর Cache ও TLB-এর বড় পরোক্ষ খরচ আলাদা করতে, আর Switch কখন Thrashing হয়ে যায় তা হিসাব করে বের করতে।
- Linux Scheduler কীভাবে চলে বলতে: Time Slice, Run Queue, Preemption, Virtual Runtime, CFS, EEVDF আর nice।
- CPU-bound আর I/O-bound কাজ, Voluntary আর Involuntary Switch চিনতে, আর
vmstatওpidstatদিয়ে সমস্যা ধরতে। - Amdahl-এর সূত্র দিয়ে Core বাড়ানোর সীমা আগেই বলে দিতে, আর আসল Core ও Hyper-Thread আলাদা করতে।
- Python-এর GIL আর Free-threaded Build বোঝাতে, আর “৪০% CPU”-তে থাকা Container কেন Throttling-এ পড়ে ভয়াবহ Latency দেয় তা বলতে।
#এই Lesson কোথায় বসে
Lesson ৩.১-এ Process আর Thread কী, আর প্রতিটা Thread-এর নিজের কী কী থাকে, তা দেখেছি। Lesson ১.২-এ ছিল Server-এর Concurrency Model আর Little-এর সূত্র, আর Lesson ১.১৩-এ Web আর App Server কীভাবে Worker চালায়।
ওই Lesson-গুলো একটা প্রশ্ন খোলা রেখে গেছে, এখানে তার উত্তর: ১৬ Core-এর একটা Machine ৩,০০০ Thread চালায় কীভাবে, আর তার দাম কত? Thread-এর সঠিক সংখ্যা কত, সেটা ঠিক পরেই, Lesson ৩.৩-এ; TLB কীভাবে কাজ করে, সেটা Lesson ৩.৪-এ। Thread-গুলো একই Data ছুঁলে কী বিপদ হয় (Race, Lock, Memory Model), সেটা Chapter 14-এর বিষয়।
#Concurrency: একসাথে অনেক কাজ চলমান
Concurrency মানে কয়েকটা কাজ এমনভাবে সামলানো, যাতে তাদের জীবনকাল একটার ওপর আরেকটা পড়ে: একটা সময়ের মধ্যে সবাই এগোয়, আর আগেরটা শেষ হওয়ার আগেই নতুনটা শুরু হতে পারে। এর মানে এই না যে সবাই একই মুহূর্তে চলছে।
Lesson ১.২-এর সেই Web Server-টাই ধরো। তিনজন User একই মুহূর্তে Request পাঠাল:
User A → Request 1
User B → Request 2
User C → Request 3
- Server যদি একটা Request পুরো শেষ করে আর বাকিদের বসিয়ে রাখে, সেটা Sequential Processing।
- আর যদি কয়েকটা Request-এর কাজ মিশিয়ে সবগুলোকেই এগিয়ে নেয়, সেটা Concurrent Processing।
এক Core-এ Scheduler দ্রুত এক কাজ থেকে আরেক কাজে সরে এটা তৈরি করে, তাই Timeline দেখতে হয় এমন:
Time →
Task A | Task B | Task A | Task C | Task B
#Parallelism: একসাথে অনেক কাজ চলছে
Parallelism মানে একই মুহূর্তে সত্যিই কয়েকটা Instruction চলছে, আলাদা আলাদা Core বা Processor-এ:
Core 1: Task A ───────────
Core 2: Task B ───────────
Core 3: Task C ───────────
তাহলে শব্দ দুটো দুটো আলাদা প্রশ্নের উত্তর। Rob Pike-এর কথাটা মনে রাখার মতো: Concurrency হলো একসাথে অনেক কিছু সামলানো; Parallelism হলো একসাথে অনেক কিছু করা। Concurrency হলো Program কীভাবে সাজানো তার বৈশিষ্ট্য, আর Parallelism হলো সেটা কীভাবে চলছে তার।
| Concurrency | Parallelism | |
|---|---|---|
| মানে | কাজগুলোর জীবনকাল একটার ওপর আরেকটা পড়ে | একই মুহূর্তে একাধিক Instruction চলে |
| কী লাগে | কাজ থামিয়ে আবার চালানোর উপায় | একের বেশি Core |
| এক Core-এ সম্ভব? | হ্যাঁ, পালা করে | না |
| মূল লাভ | দ্রুত সাড়া, আর অপেক্ষাগুলো একসাথে চালানো | CPU-র কাজ আগে শেষ হওয়া |
| কে দেয় | Design: Thread, Event Loop, Async | Hardware |
দুটো একে অন্যের ওপর নির্ভর করে না, তাই চার রকমই হয়: কোনোটাই না (সাধারণ একটা Script), শুধু Concurrent (এক Core পালা করে), শুধু Parallel (একটা বড় হিসাব কয়েক Core-এ ভাগ, কোনো Task বদল নেই), আর দুটোই (প্রায় সব Server, যা তুমি কখনো চালাবে)।
তিনটা Request, চার রকমে: একটার পর একটা, Concurrent, Parallel, আর দুটোই
প্রতিটা সারি একটা CPU Core, আর প্রতিটা বার হলো ওই Core তখন কী চালাচ্ছে। খেয়াল করো কোন Request কখন প্রথম এগোয়, আর কখন শেষ হয়।
পরিস্থিতি
এক Core, একটা Request পুরো শেষ করে তবেই পরেরটা শুরু। প্রত্যেকটার দরকার ৩০ ms CPU।
A, B আর C একই মুহূর্তে Request পাঠাল। Server A-কে ধরল, বাকিরা বসে রইল, কারণ এই Server একটা কাজ শেষ না করে অন্যটায় হাত দেয় না।
#Concurrency কেন দরকার
নোটে পাঁচটা কারণ আছে, আর সবগুলোর গোড়ার কথা একই: একটা কাজের জীবনের বেশিরভাগ সময় হিসাব করে কাটে না।
- অনেক User সামলানো। অচেনা কারও Request শেষ হওয়ার অপেক্ষায় কেউ বসে থাকবে কেন?
- I/O-র অপেক্ষার সময় অন্য কাজ করা। অপেক্ষারত Thread-এর CPU লাগে না, তাই আরেকজন সেটা ব্যবহার করতে পারে।
- Background Task চালানো, যেমন Email পাঠানো বা পরিষ্কারের কাজ, মূল কাজ না থামিয়ে।
- CPU আর I/O Resource ঠিকমতো ব্যবহার করা। Request লাইনে দাঁড়িয়ে আর Core ফাঁকা, এটা টাকা নষ্ট।
- Server-এর Throughput বাড়ানো, কখনো কখনো নতুন Hardware ছাড়াই দশ গুণ।
শেষ দাবিটা সংখ্যায় দেখো। একটা Request-এ লাগে ৫ ms CPU আর ৪৫ ms Database-এর অপেক্ষা, এক Core-এ:
| Design | প্রতি Request-এ সময় | Throughput | Core ব্যস্ত |
|---|---|---|---|
| Sequential: প্রতিটা Database-এর উত্তরের জন্য বসে থাকা | ৫০ ms | ১ / ০.০৫ সেকেন্ড = ২০ req/s | ১০% |
| Concurrent: অপেক্ষার সময় অন্যগুলো চালানো | তবু ৫০ ms | ১ / ০.০০৫ সেকেন্ড = ২০০ req/s (CPU-র সীমা) | ১০০% পর্যন্ত |
একই Core দশ গুণ কাজ করছে, অথচ কোনো Request দ্রুত হয়নি। Database Call-গুলো শুধু একটার ওপর আরেকটা পড়েছে। Lesson ১.২-এর Little-এর সূত্রে ২০০ req/s × ০.০৫ সেকেন্ড মানে একসাথে ১০টা Request, তাই প্রায় ১০টা Thread; Lesson ৩.৩-এ এটাই হবে সংখ্যা ঠিক করার নিয়ম। অবশ্য Database যদি একসাথে ওই ১০টা Query নিতে পারে, তবেই।
Concurrency-র লাভ কোথায়: Request অপেক্ষা করলে CPU ফাঁকা বসে থাকে
একটা সাধারণ Request: ৫ ms CPU, তারপর ৪৫ ms Database-এর অপেক্ষা। দুই পরিস্থিতিতেই একই এক Core।
পরিস্থিতি
Server প্রতিটা Database-এর উত্তরের জন্য বসে থাকে, তারপর পরের Request ধরে।
A ৫ ms Core ব্যবহার করে, তারপর ৪৫ ms Database-এর অপেক্ষা। ওই ৪৫ ms Core কিছুই করে না, আর B আর C লাইনে বসে থাকে।
Concurrency-র দামও আছে। মিশিয়ে চলা Code বাজে সময়ে Shared Data ছুঁয়ে ফেলতে পারে, তাতে Race হয় (Chapter 14), আর যেকোনো দুই Instruction-এর মাঝে Thread থেমে যেতে পারে, এক Core-এও। Debug করাও কঠিন, আর প্রতিটা Thread Memory খায়। আর Switch করতেও CPU লাগে, যা নিয়ে এই Lesson-এর বাকিটা।
#Context Switching
Context Switch হলো একটা Core এক Thread চালানো থামিয়ে আরেকটা শুরু করলে যা ঘটে: চলতি Thread-এর Execution State জমা থাকে, আর পরেরটার State ফিরে আসে। Core নিজে “Thread” বলে কিছু চেনে না, সে শুধু তার Register যা বলে তাই চালায়। Register বদলালে Thread বদলে যায়।
CPU
|
+---- Thread A
|
+---- Thread B
|
+---- Thread C
ধরো CPU Thread A চালাচ্ছে, আর Scheduler ঠিক করল এবার Thread B চলবে:
Kernel-এ ঢোকা
Timer Interrupt, আটকে যাওয়া System Call বা অন্য কোনো ঘটনায় Core Kernel Mode-এ আসে। আগে Thread-এর User Mode-এর Register জমা হয়, যাতে Kernel সেগুলো ব্যবহার করতে পারে।
পরের Thread বাছা
Scheduler Run Queue দেখে B-কে বাছে। আর কেউ Runnable না থাকলে Switch-ই হয় না, A চলতে থাকে।
Thread A জমা রাখা
A-র Stack Pointer, Program Counter আর Register তার Kernel Record-এ যায়, যা Lesson ৩.১-এর Process Control Block-এর ধারণারই অংশ। A Ready হয় (অপেক্ষায় থাকলে Blocked)।
Thread B ফিরিয়ে আনা
B-র জমা Stack Pointer আর Register Core-এ আসে। B অন্য Process-এর হলে তার Page Table-ও আসে।
Thread B চালানো
Kernel User Mode-এ ফেরে, আর B ঠিক যে Instruction-এ থেমেছিল, সেখান থেকেই চলে। ঘড়ি না দেখলে B টেরই পায় না যে সে থেমেছিল।
#Context-এ কী কী থাকে
| অংশ | কী এটা | কেন জমা রাখতে হয় |
|---|---|---|
| Program Counter | পরের Instruction-এর Address | যাতে Thread ঠিক জায়গা থেকে চলে |
| CPU Register | সাধারণ মান, Flag, Floating-point আর Vector Register | Thread যে মান নিয়ে কাজ করছে, সেগুলো এখানে |
| Stack Pointer | Thread-এর নিজের Stack-এর মাথা | Local Variable আর Call-এর ইতিহাস ওখানে |
| Scheduling-এর তথ্য | অবস্থা, Priority, এ পর্যন্ত কতটা CPU পেল | যাতে Scheduler ন্যায্যভাবে দেখতে পারে |
| Memory Management State | চলতি Page Table | বদলায় শুধু তখনই, যখন Switch অন্য Process-এ যায় |
Memory-র ভেতরের জিনিস জমা বা কপি হয় না। Stack আর Heap যেখানে আছে সেখানেই থাকে; Stack Pointer ফিরে পেলেই Thread তার Stack ফিরে পায়। তাই সরাসরি খরচ কম, আর Thread কত Memory ব্যবহার করছে তার ওপর সেটা নির্ভর করে না।
#কীসে Switch হয়
| কারণ | ধরন | উদাহরণ |
|---|---|---|
| Time Slice শেষ, অন্যরা অপেক্ষায় | Involuntary | CPU-ভারী Thread-এর পালা শেষ |
| বেশি Priority-র বা বেশি প্রাপ্য Thread Ready হলো | Involuntary | I/O-তে জাগা Thread হিসাবের কাজ সরিয়ে দিল |
| Thread I/O-তে আটকাল | Voluntary | Data ছাড়া Socket-এ read() |
| Thread Lock বা Condition-এ আটকাল | Voluntary | ব্যস্ত Lock-এর অপেক্ষা (Chapter 14) |
| Thread ঘুমাল বা ছেড়ে দিল | Voluntary | sleep(1), sched_yield() |
| Thread শেষ হলো | Voluntary | চালানোর আর কিছু নেই |
Voluntary মানে Thread থেমেছে কারণ আর এগোতে পারছিল না। Involuntary মানে এগোতে পারত, কিন্তু Scheduler Core নিয়ে নিল। ধীর হওয়ার সমস্যায় এই পার্থক্যটাই তোমার প্রথম সূত্র: Involuntary বেশি মানে Runnable Thread-গুলোর জন্য CPU কম; Voluntary বেশি মানে Thread-গুলো I/O বা Lock-এ অপেক্ষা করছে।
নোটে “Interrupt”-কেও কারণ হিসেবে ধরা আছে। Interrupt Kernel-এ ঢোকে, আর সে যা সামলাল তাতে বেশি জরুরি কোনো Thread জাগলে Switch হতে পারে (ওপরের মতো), কিন্তু শুধু Interrupt নিজে Context Switch না।
একটা Context Switch, ধাপে ধাপে
একটা Core একবারে একটাই Thread-এর State ধরে রাখতে পারে। Thread বদলাতে Kernel চলতি State Memory-তে কপি করে রাখে, আর আরেকটা Thread-এর State ভেতরে আনে। Stage-এর নিচের ঘরগুলো খেয়াল করো।
পরিস্থিতি
অনিচ্ছাকৃত Switch: A-র কাজ এখনো বাকি, কিন্তু তার পালা শেষ।
Core-এর ভেতরে
এখন চলছে
A
Program Counter
0x401a20
Stack Pointer
0x7f3a10
অন্য Register
rax=42, rbx=7 …
জমা রাখা Context, Kernel Memory-তে
| Thread | অবস্থা | জমা Program Counter |
|---|---|---|
| A | চলছে | Core-এর ভেতরে |
| B | Ready | 0x52c1f0 |
A চলছে। তার Program Counter, Stack Pointer আর অন্য Register এখন Core-এর ভেতরে। B Ready: সে চলতে পারে, আর Run Queue-তে অপেক্ষা করছে।
#Switch-এর আসল দাম
নোট বলে Switch-এ CPU নষ্ট হয়, Cache Locality যায়, TLB-তেও প্রভাব পড়ে। কিন্তু এই তিনটার মাপ একদম আলাদা:
| খরচ | কী ঘটে | সাধারণ মাপ |
|---|---|---|
| সরাসরি | Kernel-এ ঢোকা, Scheduler চালানো, জমা আর ফেরানো | প্রায় ১–৫ µs |
| Cache | নতুন Thread-এর Data আর L1, L2-তে নেই, দূর থেকে আনতে হয় | প্রায়ই কয়েক দশ µs ধীরে চলা, বড় Working Set হলে আরও বেশি |
| TLB | নতুন Process-এর Address অনুবাদ নেই, আবার খুঁজতে হয় | মূলত Process Switch-এ; কীভাবে তা Lesson ৩.৪-এ |
| Migration | Thread আরেক Core-এ গিয়ে পড়ে, নিজের Cache থেকে দূরে | Cache-এর খরচ বাড়ায়, CPU Socket পেরোলে আরও খারাপ |
এগুলো সীমা, ধ্রুব সংখ্যা না: CPU, Kernel-এর সেটিং আর Thread কতটা Memory ছোঁয় তার ওপর নির্ভর করে। নিজেরটা মেপে দেখো (“নিজে করে দেখো” অংশে আছে)। আসল কথা হলো পরোক্ষ খরচ সাধারণত সরাসরি খরচের চেয়ে বেশি, আর দুটো মিলিয়েও কম শুধু দুই Switch-এর মাঝে লম্বা চলার তুলনায়। ধরো প্রতি Switch-এ মোট ২৫ µs লাগে:
| অবস্থা | Core-প্রতি সেকেন্ডে Switch | নষ্ট সময় |
|---|---|---|
| অল্প কয়েকটা CPU-bound Thread, ৩ ms Slice | প্রায় ৩৩০ | ৩৩০ × ২৫ µs ≈ সেকেন্ডে ৮ ms, ১%-এর কম |
| ব্যস্ত API Server, প্রতি জাগায় প্রায় ১ ms কাজ | প্রায় ১,০০০ | সেকেন্ডে ২৫ ms, ২.৫% |
| হাজার হাজার Thread, প্রতি জাগায় ২৫ µs কাজ | ২০,০০০ | সেকেন্ডে ৫০০ ms, ৫০% |
#Thrashing
এখানে Thrashing মানে Machine Switch আর Cache ভরায় এত সময় দিচ্ছে যে আসল কাজ কমে যাচ্ছে, আর Thread বাড়ালে আরও কমছে। দেখবে CPU ১০০%, vmstat-এ cs বিশাল, আর Load বাড়লে Throughput পড়ছে। (Paging থেকে হওয়া Memory-র Thrashing আলাদা জিনিস, নামটা এক: Lesson ৩.৪ দেখো।)
এটা দুই রকম, আর সমাধানও এক না:
- অনেক ছোট ছোট জাগা। প্রতিটা Thread জাগে, একেবারে ছোট একটা কাজ করে আবার আটকায়: সেকেন্ডে হাজার হাজার বার। বাড়তি খরচ CPU খেয়ে ফেলে। সমাধান: প্রতি জাগায় বেশি কাজ, মানে Batch, Event Loop বা কম Thread।
- অনেক Runnable Thread। ৪ Core-এ এক হাজার CPU-bound Thread: প্রতিটা Slice তবু কয়েক ms, তাই Switch-এর খরচ কম দেখায়, কিন্তু প্রতিটা Thread পরের পালার জন্য অপেক্ষা করে ২৫০ × ৩ ms = ৭৫০ ms, আর সবসময় শুরু করে ঠান্ডা Cache নিয়ে। সমাধান: Core-এর সমান সংখ্যক চালাও, বাকিরা Queue-তে থাকুক। এই Queue-ই Thread Pool, নোট এজন্যই ওদিকে ইঙ্গিত করেছে।
তৃতীয় একটা ধরন দেখতে একই রকম: Lock Convoy। Thread-গুলো একটা ব্যস্ত Lock-এ লাইন দেয়, প্রতিবার হাত বদলাতে Switch লাগে, তাই Thread বাড়ালে Throughput পড়ে, অনেক সময় CPU পুরো ভরার আগেই।
Switch-এর আসল দাম: হিসাবে না আসা থেকে Thrashing পর্যন্ত
ধরো প্রতি Switch-এ সরাসরি ৫ µs, আর পরে ঠান্ডা Cache-এর কারণে ২০ µs ধীর। আসল কথা হলো, দুই Switch-এর মাঝে কতটা কাজ হয়।
পরিস্থিতি
অল্প কয়েকটা CPU-bound Thread, প্রত্যেকে ৩ ms Slice চালায়। লাল আর হলুদ অংশ বাস্তবের চেয়ে অনেক চওড়া করে আঁকা, নইলে চোখেই পড়ত না।
প্রতি ৩ ms-এ একটা Switch মানে সেকেন্ডে প্রায় ৩৩৩টা। Cache-এর প্রভাব ধরে প্রতিটায় প্রায় ২৫ µs, তাই প্রতি সেকেন্ডে নষ্ট হয় প্রায় ৮ ms।
নিজেকে যাচাই করোপ্রশ্ন
Request লাইনে জমছে দেখে একটা দল Service-এর Thread ২০০ থেকে ২,০০০ করল। CPU আগেই ৯৫%-এ ছিল। পরিবর্তনের পর CPU ১০০%, vmstat-এর cs দশ গুণ, আর Throughput আগের চেয়ে কম। কী হলো?
#Scheduler: এরপর কে চলবে
Scheduler হলো Kernel-এর সেই অংশ, যে ঠিক করে প্রতিটা Core কোন Runnable Thread চালাবে, আর কতক্ষণ। Linux প্রতিটা Thread-কে আলাদা করে Schedule করে: এক Thread-এর Process শুধু তার সহজ রূপ। (I/O-তে আটকে থাকা Thread জাগার আগে এই হিসাবে থাকেই না।)
- Run Queue। প্রতিটা Core-এর নিজের একটা Ready Thread-এর Queue। আলাদা Queue থাকায় Scheduling দ্রুত হয়, এক তালিকা নিয়ে Core-গুলোর কাড়াকাড়ি নেই। ফাঁকা Core ব্যস্ত Core থেকে কাজ টেনে নেয়, তাতে Cache-এর গরম ভাবটা যায়।
- Time Slice। অন্যরা অপেক্ষায় থাকলে একটা Thread কতক্ষণ চলতে পারে: সাধারণ Server-এ কয়েক ms। এটা একটা আপস: ছোট Slice মানে সবার অপেক্ষা কম, কিন্তু Switch বেশি; লম্বা Slice মানে উল্টো।
- Preemption। Thread রাজি না থাকলেও Kernel তার থেকে Core নিয়ে নিতে পারে, কারণ Timer Interrupt তাকে Kernel-এ ফিরিয়ে আনে। এজন্যই একটা বাজে Infinite Loop পুরো Machine জমিয়ে দেয় না।
#CFS থেকে EEVDF
CFS, মানে Completely Fair Scheduler, ২০০৭ সালে Kernel ২.৬.২৩ থেকে Linux-এর ডিফল্ট ছিল। ধারণাটা সহজ। প্রতিটা Thread-এর একটা Virtual Runtime আছে: সে যতটা CPU পেয়েছে, Priority মিলিয়ে। CFS সবসময় চালায় যার Virtual Runtime সবচেয়ে কম, মানে যে সবচেয়ে পিছিয়ে। দ্রুত খুঁজতে Runnable Thread-গুলো Red-black Tree-তে সাজানো থাকত।
EEVDF, মানে Earliest Eligible Virtual Deadline First, এর জায়গা নিল Kernel ৬.৬-এ (২০২৩-এর শেষে)। Virtual সময়ের ন্যায্যতা একই থাকে, বদলায় পরের Thread বাছার নিয়ম, দুই ধাপে:
১. Eligible: যারা নিজের ন্যায্য ভাগের বেশি পায়নি (Lag শূন্য বা তার বেশি), শুধু তারাই বাছাইয়ে থাকে। ২. সবচেয়ে আগের Virtual Deadline: তাদের মধ্যে যার Deadline সবার আগে, সে চলে। Deadline হলো তার Virtual Runtime আর চাওয়া Slice-এর যোগফল, Weight মিলিয়ে।
লাভ: একটা Thread ছোট Slice চেয়ে ছোট আর ঘন ঘন পালা পেতে পারে, তাতে ভাগ না বাড়িয়েই তার Latency কমে। CFS এটা অনেক জোড়াতালি দিয়ে কাছাকাছি আনত। Scheduler খুব কম Engineer-ই Tune করে, কিন্তু নামগুলো Interview আর Kernel Changelog-এ আসে, আর পরের Thread বাছার ক্ষেত্রে “CFS” যে পুরনো কথা, সেটা জানা চাই। নামটা এখনো টিকে আছে CFS Bandwidth Control-এ, যা Container-এর Quota-র পেছনের ব্যবস্থা, এই Lesson-এই পরে আসছে। Kernel ৬.১২ থেকে sched_ext-ও আছে, যা দিয়ে eBPF-এ লেখা নিজের Scheduler চালানো যায়।
#Priority
- nice চলে −২০ (এগিয়ে) থেকে ১৯ পর্যন্ত। এটা Thread-এর Weight বদলায়, তাই কাড়াকাড়ির সময় তার ভাগও বদলায়: nice 0-তে Weight ১০২৪, nice 5-এ ৩৩৫, তাই এই দুজন একটা Core ভাগ করে প্রায় ৭৫% আর ২৫%। প্রতি ধাপে প্রতিদ্বন্দ্বী Thread-এর মধ্যে CPU-র প্রায় ১০% তফাত হয়। কেউ প্রতিযোগিতায় না থাকলে nice 19-এর Thread-ও পুরো Core পায়।
- Real-time Class (
SCHED_FIFO,SCHED_RR,SCHED_DEADLINE) সবসময় সাধারণ Thread-এর আগে চলে। এগুলো Audio বা Control Loop-এর জন্য। Linux সাধারণত প্রায় ৫% সাধারণ Thread-এর জন্য রেখে দেয়, তাই ঘুরপাক খাওয়া Real-time Thread Core পুরোপুরি আটকায় না; তবু সাধারণ Service সাধারণ Class-এই থাকা উচিত।
Scheduler: এরপর কে চলবে, আর কতক্ষণ
প্রতিটা Core-এর নিজের একটা Run Queue আছে, তাতে Ready Thread-গুলো থাকে। একটা ন্যায্যতার হিসাব দিয়ে Scheduler সেখান থেকে বাছে। Blocked Thread কোনো Run Queue-তে থাকে না। টেবিলটা খেয়াল করো।
পরিস্থিতি
এক Core-এ তিনটা CPU-bound Thread, সমান Priority। নিয়ম: এ পর্যন্ত যে সবচেয়ে কম CPU পেয়েছে, সে চলবে।
Core 0-এর Run Queue
| Thread | Virtual Runtime | অবস্থা |
|---|---|---|
| T1 | 10 ms | বাছাই: সবচেয়ে কম |
| T2 | 11 ms | Ready |
| T3 | 12 ms | Ready |
প্রতিটা Thread-এর একটা Virtual Runtime আছে: সে কতটা CPU পেয়েছে, Priority মিলিয়ে। সবচেয়ে পিছিয়ে T1, তাই সে-ই পরে। ২০০৭ থেকে Linux-এর CFS-এর মূল ধারণা এটাই।
#CPU-bound আর I/O-bound কাজ
এই Lesson-এর প্রতিটা Design-এর সিদ্ধান্ত নির্ভর করে তোমার Thread-গুলো সময়টা কীভাবে কাটায় তার ওপর:
| CPU-bound | I/O-bound | |
|---|---|---|
| সময় কাটে | হিসাবে | Network, Disk, Database-এর অপেক্ষায় |
| উদাহরণ | ছবি ছোট করা, Password Hash, Compression, JSON Parse, Machine Learning Inference | সাধারণ API Handler, Proxy, Chat Server, Web Scraper |
| সাধারণত Switch-এর ধরন | Involuntary | Voluntary |
| Core-এর চেয়ে বেশি Thread? | লাভ নেই, শুধু বাড়তি খরচ | হ্যাঁ: অপেক্ষাগুলো একসাথে চলে |
| সীমা | CPU | যার জন্য অপেক্ষা, তারপর Memory |
| Scheduler কী করে | ন্যায্য Slice দেয় | প্রায়ই জাগার পরপরই চালায় |
বেশিরভাগ Server মূলত I/O-bound, তবু ধরে নিয়ো না। Service খুব কমই পুরোপুরি এক রকম হয়, আর মিশ্রণটা মাপা যায়: pidstat -u দেখায় প্রতিটা Thread কতটা CPU খায়, আর যে Thread Core-এর ৫% খায়, সে বাকি ৯৫% অপেক্ষায় থাকে।
বিপদ হলো মিশ্রণ। দ্রুত I/O Handler-এর সাথে একই Pool-এ কয়েকটা CPU-ভারী কাজ থাকলে Run Queue ভরে যায়, আর দ্রুত Handler-গুলো Ready হয়েই বসে থাকে, চলে না। CPU Chart বলে “১০০%”, আর সস্তা Request-গুলোর p99 Latency অকারণে বাড়ে। এদের আলাদা Pool, Process বা Service-এ রাখো।
CPU-bound বনাম I/O-bound: কখন Thread বাড়ালে লাভ, কখন ক্ষতি
একই প্রশ্নের উত্তর উল্টো হতে পারে। নির্ভর করে প্রতিটা Thread সময়টা কীসে কাটায় তার ওপর। এখানে সারি মানে Thread, Core লেখা না থাকলে।
পরিস্থিতি
এক Core। প্রতিটা Request: ৫ ms CPU, ২০ ms অপেক্ষা। পাঁচটা Thread ঠিক খাপ খায়: ১ + ২০/৫।
একটা Thread অপেক্ষা শুরু করতেই পরেরটা চলে। ধূসর বার বেশি মানে Thread-গুলো CPU ব্যবহার করছে না, তাই এই Core অনেক Thread বইতে পারে।
#Core বাড়ালেই কেন লাভ হয় না: Amdahl-এর সূত্র
Parallelism শুধু কাজের সেই অংশটাই দ্রুত করে, যা Parallel চলতে পারে। Amdahl-এর সূত্র (১৯৬৭) এটাকে সংখ্যায় আনে। কাজের s অংশ যদি Serial চলতেই হয়, N Core-এ Speedup হবে:
Speedup(N) = 1 / (s + (1 − s) / N)
Limit as N grows = 1 / s
| Parallel অংশ | ২ Core | ৪ Core | ৮ Core | ১৬ Core | ৬৪ Core | সীমা |
|---|---|---|---|---|---|---|
| ৫০% | ১.৩৩× | ১.৬× | ১.৭৮× | ১.৮৮× | ১.৯৭× | ২× |
| ৯০% | ১.৮২× | ৩.০৮× | ৪.৭১× | ৬.৪× | ৮.৭৭× | ১০× |
| ৯৫% | ১.৯× | ৩.৪৮× | ৫.৯৩× | ৯.১৪× | ১৫.৪× | ২০× |
| ৯৯% | ১.৯৮× | ৩.৮৮× | ৭.৪৮× | ১৩.৯× | ৩৯.৩× | ১০০× |
তাহলে ৯৫% Parallel একটা কাজ কখনোই ২০ গুণের বেশি দ্রুত হবে না, আর ৬৪ Core-এ পাওয়া যায় মাত্র ১৫.৪ গুণ। Serial অংশ যত ছোটই দেখাক, ছাদটা সে-ই ঠিক করে।
Server-এ “Serial” মানে সাধারণত একটা লম্বা কাজ না। মানে প্রতিটা Request-কে যা একবারে একজন করে করতে হয়: একটা Global Lock ধরা, এমন Row যা সব Request আপডেট করে, এক Thread-এর Log Writer, Shared কোনো Service-এ একটা Connection। Request স্বাধীন হলে প্রায় সরলরেখায় Scale করে, কারণ এখানে একই কাজ দ্রুত করা না, বেশি কাজ করা হচ্ছে (Gustafson-এর পর্যবেক্ষণ)। একটা Lock ভাগ করলেই আবার Amdahl-এর আওতায়: Request তার সময়ের ২৫% Lock ধরে থাকলে চার Core-এই পথ শেষ।
আসল System সমান থাকার চেয়েও খারাপ হতে পারে। Core-গুলো সমন্বয়েও সময় দেয়: একই Cache Line নিয়ে কাড়াকাড়ি, একে অন্যকে জাগানো, Data মিলিয়ে রাখা। Universal Scalability Law এই খরচটা যোগ করে, আর সেটা Core-এর জোড়ার সংখ্যার সাথে বাড়ে, তাই Throughput একটা চূড়ায় উঠে পড়তে পারে। এজন্যই দলগুলো আসল মাপ পর্যন্ত Load Test করে, আর এজন্যই Vertical Scaling-এর শেষ আসে Machine-এর তালিকা ফুরোনোর অনেক আগে।
Amdahl-এর সূত্র: Serial অংশই সীমা ঠিক করে
Speedup = ১ / (s + (১ − s) / N), এখানে s হলো যে অংশ Parallel চলতে পারে না। একটা কাজ এক Core-এ নেয় ১০০ ms। দেখো, Core বাড়ালে কী পাও।
পরিস্থিতি
Input পড়া আর ফল মেলানো এক Core-এই চলে। মাঝের অংশ Core-গুলোতে ভাগ হয়।
এক Core: ২০ ms Serial হতেই হবে, ৮০ ms ভাগ করা যায়। মোট ১০০ ms।
#Core, Hyper-Thread আর vCPU
nproc গোনে Logical CPU, Core না, তাই সাবধানে পড়ো:
- আসল Core: নিজের কাজের যন্ত্রাংশ, নিজের L1 আর L2 Cache।
- Hyper-Thread (SMT): একটা আসল Core দুই সেট Register রেখে নিজেকে দুটো Logical CPU হিসেবে দেখায়। কাজের যন্ত্রাংশ ভাগাভাগি। এক Thread Memory-র অপেক্ষায় থাকলে অন্যটা সেগুলো ব্যবহার করতে পারে, তাই জোড়ায় সামান্য লাভ হয়, প্রায়ই ১০–৩০%, কখনো কিছুই না, দ্বিগুণ তো না-ই।
- vCPU: Cloud তোমাকে যা বেচে। অনেক x86 Instance-এ এক vCPU মানে এক Hyper-Thread, তাই ২ vCPU প্রায়ই একটা আসল Core; AWS Graviton-এ এক vCPU পুরো একটা Core। Hypervisor এগুলো কীভাবে ভাগ করে, Steal Time সহ, সেটা Chapter 4-এ।
lscpu চালিয়ে “Thread(s) per core” দেখো। CPU-bound Benchmark যদি “১৬ CPU”-র Machine-এ ৮ পর্যন্ত ভালো বেড়ে তারপর অনেক সমান হয়ে যায়, সম্ভবত সেটা Hyper-Thread। Latency আর নিরাপত্তা নিয়ে সংবেদনশীল কিছু System SMT বন্ধ রাখে।
#যখন ভাষাই বাধা: Python-এর GIL
CPython, মানে স্ট্যান্ডার্ড Python-এ একটা Global Interpreter Lock (GIL) আছে: Python Bytecode চালাতে Thread-কে এটা ধরতে হয়, তাই যত Core-ই থাক, একবারে একটাই Thread চালায়। এতে Interpreter-এর ভেতরের জিনিস, যেমন Reference Count, সহজ আর নিরাপদ থাকে। ফল:
| কাজ | Thread (ডিফল্ট Build) | কী ব্যবহার করবে |
|---|---|---|
| I/O-bound | ভালো চলে: Blocking Call-এ GIL ছাড়া হয় | Thread বা asyncio |
| পুরো Python-এ CPU-bound | গতি বাড়ে না, প্রায়ই একটু কমে | multiprocessing, বা কয়েকটা Worker Process সহ Task Queue |
| Native Library-তে CPU-bound | প্রায়ই Scale করে, Library GIL ছাড়লে (NumPy, অনেক Compression আর Crypto Library) | Thread, মেপে দেখার পর |
Gunicorn-এর মতো Python Web Server এজন্যই কয়েকটা Worker Process চালায়: প্রতি Core-এ একটা, প্রত্যেকের নিজের GIL।
Free-threaded Build GIL-কে ঐচ্ছিক করে। PEP 703 এটাকে Python ৩.১৩-এ পরীক্ষামূলকভাবে আনে, আর ৩.১৪ থেকে এটা আনুষ্ঠানিকভাবে সমর্থিত (PEP 779), যদিও ডিফল্ট Build-এ GIL এখনো আছে। Binary-র নাম সাধারণত python3.14t। সেখানে Thread Shared Memory নিয়ে Parallel চলে, তবে দাম আছে: এক Thread-এর Code ধীর হয় (৩.১৪-এ প্রায় ৫–১০%), C Extension-কে সমর্থনের কথা জানাতে হয়, নইলে Import করলেই GIL ফিরে আসে, আর যে Code চুপচাপ GIL-এর ওপর ভর করেছিল, তাতে সত্যিকারের Race হতে পারে। এটা জেনেশুনে নেওয়ার সিদ্ধান্ত, আর প্রতি Release-এ দেখে নেওয়ার মতো।
অন্য ভাষাতেও এমন আছে। স্ট্যান্ডার্ড Ruby-তে একই রকম Global Lock আছে। Node.js Program তার JavaScript চালায় এক Thread-এ, যা Lesson ৩.৬-এ দেখবে। Go আর Java-তে এমন Lock ছাড়াই Thread Parallel চলে, আর দুটোতেই হালকা User-level Thread আছে, যা Kernel-এ না ঢুকেই Switch করে; সেটাও Lesson ৩.৬-এ।
Python-এর GIL: Thread পালা করে চলে, কিন্তু Python Code Parallel চলে না
৩টা ফাঁকা Core-এর একটা Machine। প্রতিটা সারি একটা Thread বা Process। একই কাজ প্রতিটা উপায়ে মিলিয়ে দেখো।
পরিস্থিতি
তিনটা Thread, প্রত্যেকে ১৫ ms পুরো Python চালায়। ডিফল্ট Build-এ Interpreter-এর চারপাশে একটা Lock আছে।
যে Thread-এর হাতে GIL, শুধু সে-ই Python Bytecode চালায়। পাশে Core ফাঁকা থাকলেও বাকিরা Ready হয়েই আটকে থাকে। হাতে থাকা Thread-কে প্রায় প্রতি ৫ ms-এ ছেড়ে দিতে বলা হয়, এটাই ডিফল্ট Switch Interval।
#Container-এর CPU Limit আর Throttling
এই Lesson-এর ধারণাগুলো সবচেয়ে ভালো ব্যাখ্যা করে এই Production সমস্যাটা। Linux-এ Container-এর CPU Limit মানে কম Core না। মানে প্রতি Period-এ CPU সময়ের একটা বাজেট, যা cgroups দিয়ে CFS Bandwidth Control চালায়:
cpu.max = 200000 100000 # 200 ms of CPU per 100 ms period: "2 CPUs"
(Docker-এ --cpus=2 আর Kubernetes-এ limits.cpu: 2 এটাই লিখে দেয়। cgroup v1-এ ছিল cpu.cfs_quota_us আর cpu.cfs_period_us।)
Container-এর সব Thread একই বাজেট থেকে খরচ করে, আর তারা একসাথে চলতে পারে। বড় Host-এ ৮টা ব্যস্ত Thread ২০০ ms-এর বাজেট উড়িয়ে দেয় আসল সময়ের মাত্র ২৫ ms-এ। তারপর চারদিকে Core ফাঁকা থাকলেও Kernel বাকি ৭৫ ms Container-এর প্রতিটা Thread থামিয়ে রাখে। তখন যে Request আসে, সে অপেক্ষা করে। লক্ষণ:
- p99 Latency-তে কয়েক দশ ms-এর ধাপ, অথচ গড় CPU ঠিকঠাক দেখায় (ধরো Limit-এর ৪০%), কারণ ১৫ সেকেন্ডের গড় ১০০ ms-এর Period দেখতে পায় না।
cpu.stat-এnr_throttledআরthrottled_usecবাড়ে। Kubernetes-এ cAdvisor-এর Metric হলোcontainer_cpu_cfs_throttled_periods_total।- Thread যত বেশি, তত খারাপ; আর যে Runtime Host-এর Core দেখে Worker Pool বানায়, তাতে আরও খারাপ। Java বহু বছর ধরে Container Limit ধরতে পারে, আর Go ১.২৫ থেকে ডিফল্ট
GOMAXPROCSLimit দেখে ঠিক করে, কিন্তু পুরনো Runtime আর হাতে লেখা Pool হয়তো পারে না। দেখে নাও, আর নিজে সংখ্যাটা ঠিক করে দাও।
Request আর Limit আলাদা। Kubernetes-এর CPU Request হয়ে যায় একটা Weight (cpu.weight): Core নিয়ে কাড়াকাড়ির সময় তোমার ভাগ, আর Pod কোথায় বসবে, সেটা ঠিক করে; ফাঁকা CPU কখনো নষ্ট করে না। Limit হলো শক্ত বাজেট, আর সেটা নষ্ট করে।
কী করবে: CPU-bound Worker Pool-এর মাপ ঠিক করো Limit দেখে, Host দেখে না। CPU-র একই Dashboard-এ Throttling রাখো। ঝাঁকে ঝাঁকে Traffic-এর Service-এ Limit বাড়াও, আর Latency-র দিক থেকে জরুরি Service-এ Limit তুলে Request-এর ওপর ভরসা করো; প্রতিবেশীদের বিশ্বাস করা গেলে এটা খুব চলে (না গেলে Limit রাখো)।
Container-এর CPU Limit: “৪০% CPU” Service-এর Latency কেন ভয়াবহ হতে পারে
CPU Limit হলো প্রতি ১০০ ms-এ CPU সময়ের একটা বাজেট। cpu.max 100000 100000 মানে প্রতি ১০০ ms-এ ১০০ ms CPU: “১ CPU”। এটা একটা Core না। সব Thread একই বাজেট থেকে খরচ করে।
পরিস্থিতি
বড় একটা Host-এ ১ CPU Limit-এর Container। চারটা Thread Runnable, চারটা Core ফাঁকা।
চারটা Thread চারটা আসল Core-এ Parallel চলে। দেয়ালঘড়ির প্রতি ms-এ তারা মিলে বাজেটের ৪ ms খরচ করে, তাই ১০০ ms-এর বাজেট ২৫ ms-এই শেষ।
#আসল Machine-এ দেখা
vmstat 1 পুরো Machine-এর ছবি দেয়:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
14 0 0 812304 90212 410232 0 0 2 18 9120 48211 71 22 7 0 0
| Column | মানে | কীভাবে পড়বে |
|---|---|---|
r | চলমান বা Ready Thread | অনেকক্ষণ ধরে Core-এর চেয়ে বেশ বেশি: Thread CPU-র জন্য লাইনে |
b | Uninterruptible Sleep-এ থাকা Thread | সাধারণত Disk I/O |
cs | সেকেন্ডে Context Switch | নিজের স্বাভাবিকের সাথে মেলাও, কারণ “বেশি” নির্ভর করে Core আর কাজের ওপর |
in | সেকেন্ডে Interrupt | Switch থেকে আলাদা গোনা |
us, sy | User আর Kernel-এর CPU | us-এর তুলনায় sy বাড়লে হতে পারে বেশি Switch বা System Call |
wa | I/O-র অপেক্ষায় ফাঁকা | সীমা Disk, CPU না |
st | Steal | Hypervisor তোমার CPU আরেকজনকে দিয়েছে (Chapter 4) |
cs-এর কোনো সর্বজনীন খারাপ সংখ্যা নেই: ব্যস্ত Multi-core Server-এ সেকেন্ডে কয়েক দশ হাজার স্বাভাবিক। আসল কথা হলো Core-প্রতি Switch কত, স্বাভাবিকের থেকে কতটা বদলেছে, আর Throughput পড়ছে কি না।
এরপর pidstat -w দেখায় কে করছে:
$ pidstat -wt -p 4213 1
TID cswch/s nvcswch/s Command
4213 0.00 0.00 java
4218 820.00 3.00 |__http-worker-1
4240 2.00 410.00 |__report-thread
cswch/s গোনে Voluntary Switch, nvcswch/s Involuntary। এখানে http-worker-1 বেশিরভাগ সময় অপেক্ষা করে (I/O-bound), আর report-thread-কে বারবার Core থেকে সরানো হচ্ছে (CPU-bound, প্রতিযোগিতায় আছে)।
| লক্ষণ | সম্ভাব্য মানে | এরপর কী দেখবে |
|---|---|---|
nvcswch/s বেশি, CPU বেশি, r Core-এর ওপরে | CPU-র তুলনায় Runnable Thread বেশি | Thread কমাও বা CPU বাড়াও; runqlat অপেক্ষা মাপে |
cswch/s বেশি, CPU কম, Request ধীর | I/O বা Lock-এর অপেক্ষা | কীসের অপেক্ষা: Thread Dump, Database, Lock Profiler |
cs খুব বেশি, CPU ১০০%, Load-এর সাথে Throughput পড়ে | Thrashing | কম Thread, Batch, Event Loop |
গড় CPU কম, p99-এ ধাপ, nr_throttled বাড়ছে | Container Throttling | Limit বনাম Thread-এর সংখ্যা |
| ১৬ “CPU”-র ৮টা ভালো চলে, বাকিগুলো সামান্যই যোগ করে | Hyper-Thread | lscpu; পরিকল্পনায় ৮ Core ধরো |
| Core বাড়লেও Throughput সমান, CPU ভরা না | Serial বাধা (Amdahl) | Lock Profile, Shared Resource |
#System Design-এ
- আগে বলো কাজটা কী ধরনের। CPU-bound না I/O-bound, এটাই বাকি সব ঠিক করে। Interviewer “কত Thread?” জিজ্ঞেস করলে পাল্টা এই প্রশ্নটাই চায়।
- কাজ দেখে Model বাছো:
| কাজ | ভালো মানায় | কেন |
|---|---|---|
| CPU-bound Job | Worker ≈ Core (বা CPU Limit), সামনে একটা Queue | Thread বাড়ালে শুধু বাড়তি খরচ |
| Blocking I/O, মাঝারি Concurrency, সহজ Code | অপেক্ষার অনুপাত দেখে মাপা Thread Pool (৩.৩) | ওপর থেকে নিচে Code; Overlap-টা Kernel করে দেয় |
| অনেক, বেশিরভাগ অলস Connection | Non-blocking I/O সহ Event Loop (৩.৫, ৩.৬) | Connection-প্রতি Thread নেই, Switch অনেক কম |
| অনেক Blocking Task, Code সহজ রাখতে চাও | Goroutine বা Java Virtual Thread (৩.৬) | User Space-এ Switch, প্রতি Task-এ আলাদা Kernel Thread লাগে না |
| মেশানো কাজ | আলাদা Pool বা Service | CPU-র কাজ দ্রুত Request-কে আটকাতে পারে না |
- Serial অংশকেই Scale-এর সীমা ধরো। Shared Lock বা একটাই Primary Database মানে তোমার Architecture-এ Amdahl-এর সূত্র। Server বাড়ানোর আগে এটা খোঁজো, আর পারলে ভাগ করো (Sharding, Core-প্রতি Data; Load Balancing আছে Chapter 25-এ)।
- কিছুটা CPU ফাঁকা রাখো। প্রায় পুরো ব্যবহারে Run Queue দ্রুত বাড়ে, তাই CPU ১০০% দেখানোর অনেক আগেই Latency বাড়ে। Lesson ১.২-এর সেই Queue-র আচরণই এটা।
- Container-এ বাজেট দেখো। Worker-এর সংখ্যা, Garbage Collector-এর Thread আর Connection Pool, সবই CPU Limit দেখে ঠিক হওয়া উচিত।
- Interview-এর প্রশ্ন: “Concurrency বনাম Parallelism?” (গঠন বনাম চালানো; Timeline-এ দুটোই দেখাও)। “Thread বাড়ালে Throughput কমে কেন?” (Switch, ঠান্ডা Cache, লাইন, কাড়াকাড়ি)। “Context Switch-এর দাম কত?” (সরাসরি ১–৫ µs, পরোক্ষ সাধারণত বেশি, আর নির্ভর করে দুই Switch-এর মাঝের কাজের ওপর)। “ছবি ছোট করতে Python Thread?” (না; Process)। “আমাদের Kubernetes Service-এ CPU ৪০%, p99 খারাপ, কেন?” (Throttling;
nr_throttledদেখো)। “Scale Up-কে কী আটকায়?” (Amdahl, তারপর সমন্বয়)।
#নিজে করে দেখো
# Overall picture: watch r, cs, us, sy while you run something heavy
vmstat 1
# Logical CPUs, and whether they are hyper-threads
nproc
lscpu | grep -E 'Thread|Core|Socket|Model name'
# Switch rates per thread of one process (pidstat is in the sysstat package)
pidstat -wt -p <PID> 1
# The counters behind those numbers (one thread, here the main thread)
grep ctxt /proc/<PID>/status
# Measure context switching: two tasks pass a message through a pipe
perf bench sched pipe -l 100000
# The same, pinned to one core, so every message forces a switch
taskset -c 0 perf bench sched pipe -l 100000
# Count switches, migrations and cache misses of a command
perf stat -e context-switches,cpu-migrations,cache-misses -- sleep 5
# See which scheduler era you are on: 6.6 or later means EEVDF
uname -r
# Container: what is the limit, and has it been throttled? (inside, cgroup v2)
cat /sys/fs/cgroup/cpu.max
grep -E 'nr_periods|nr_throttled|throttled_usec' /sys/fs/cgroup/cpu.stat
# Priorities: run a job at low priority, or pin it to cores 0 and 1
nice -n 10 ./batch-job
taskset -c 0,1 ./batch-job
Amdahl-এর সূত্র আর GIL নিজে দেখো। এটা gil.py নামে রেখে চালাও। ডিফল্ট Build-এ Thread Pool-এ প্রায় Serial চালানোর সমান বা বেশি সময় লাগবে, আর Multi-core Machine-এ Process Pool অনেক দ্রুত হবে:
import time
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
def burn(n: int) -> int:
total = 0
for i in range(n):
total += i * i
return total
def timed(label: str, run) -> None:
start = time.perf_counter()
run()
print(f"{label:<10} {time.perf_counter() - start:.2f} s")
if __name__ == "__main__":
N, WORKERS = 5_000_000, 4
timed("serial", lambda: [burn(N) for _ in range(WORKERS)])
with ThreadPoolExecutor(WORKERS) as pool:
timed("threads", lambda: list(pool.map(burn, [N] * WORKERS)))
with ProcessPoolExecutor(WORKERS) as pool:
timed("processes", lambda: list(pool.map(burn, [N] * WORKERS)))
এরপর burn-এর ভেতরটা বদলে time.sleep(1) করে দেখো: এবার Thread একদম ঠিকঠাক একসাথে চলবে, কারণ অপেক্ষার সময় GIL ছাড়া থাকে।
#সারসংক্ষেপ
মূল কথা
- Concurrency = কাজগুলো একই সময়ে চলমান (Design); Parallelism = একই মুহূর্তে Instruction চলা (Hardware)। Concurrency এক Core-এ সীমাবদ্ধ না। Parallelism-এ একের বেশি লাগে।
- এক Core-এ পুরো CPU-র কাজ টুকরো করলে মেলে ন্যায্যতা, গতি না। I/O-র অপেক্ষা একসাথে চালালে Throughput কয়েক গুণ হতে পারে: ৫ ms CPU + ৪৫ ms অপেক্ষা একই Core-এ ২০ থেকে ২০০ req/s হয়।
- Context Switch এক Thread-এর Register, Stack Pointer আর Program Counter জমা রাখে, আরেকটার ফেরায়। Memory কপি হয় না। Process Switch-এ Page Table-ও বদলায়।
- Voluntary Switch মানে I/O বা Lock-এ অপেক্ষা; Involuntary মানে CPU নিয়ে প্রতিযোগিতা। শুধু System Call বা Interrupt হলে সেটা Mode Switch মাত্র।
- সরাসরি খরচ প্রায় ১–৫ µs, আর Cache ও TLB-এর প্রভাব সাধারণত বেশি। আসল কথা দুই Switch-এর মাঝের কাজ। Thrashing মানে Switch কাজকে ঠেলে সরায়, তাই Thread বাড়ালে Throughput কমে।
- Linux Scheduler Core-প্রতি একটা Run Queue রাখে, কয়েক ms-এর Time Slice দেয়, যে কাউকে Preempt করতে পারে। CFS চালাত সবচেয়ে কম Virtual Runtime; EEVDF (৬.৬ থেকে) চালায় Eligible-দের মধ্যে সবচেয়ে আগের Deadline। nice ভাগ বদলায়, শুধু কাড়াকাড়ির সময়।
- CPU-bound: Thread ≈ Core। I/O-bound: Core ভরা পর্যন্ত Thread বাড়ালে লাভ। মেশানো: আলাদা করো।
- Amdahl: Speedup সর্বোচ্চ ১ / s; ৯৫% Parallel-এর ছাদ ২০ গুণ। Server-এ Serial অংশ সাধারণত একটা Lock বা Shared Resource। সমন্বয়ের খরচে Throughput পড়তেও পারে।
- Hyper-Thread যোগ করে হয়তো ১০–৩০%, দ্বিগুণ না; vCPU প্রায়ই একটা Hyper-Thread।
- ডিফল্ট Build-এ Python-এর GIL Python Bytecode-কে Parallel চলতে দেয় না: CPU-র কাজে Process, I/O-তে Thread। Free-threaded Build আছে, ঐচ্ছিক।
- Container-এর CPU Limit হলো প্রতি ১০০ ms-এ সময়ের বাজেট, Core না: বেশি Thread আগেই সেটা উড়িয়ে দেয়, তারপর সব থামে। শুধু CPU না,
nr_throttled-ও দেখো। Request হলো Weight, Limit হলো ছাদ।
আগে নিজে উত্তর দাও, তারপর ব্যাখ্যাটা পড়ো। ঠিক হলেও পড়ো, কারণ ভুল Option-গুলো কেন ভুল, সেটাও ওখানে আছে।
নিজেকে যাচাই করোপ্রশ্ন
Concurrency-র সবচেয়ে ভালো সংজ্ঞা কোনটা?
নিজেকে যাচাই করোপ্রশ্ন
একটা ছোট Virtual Machine-এ ঠিক একটাই Core, তাতে ৫০ Thread-এর Web Server চলে। কোনটা ঠিক?
নিজেকে যাচাই করোপ্রশ্ন
এক সহকর্মী বলল: আমাদের ৩২ Core-এর Server ২,০০০ Thread চালায়, তাই এটা Parallelism, Concurrency না, কারণ Concurrency শুধু এক Core-এর জন্য। ঠিক উত্তর কী?
নিজেকে যাচাই করোপ্রশ্ন
একটা Handler ২ ms CPU খায়, তারপর Database-এর জন্য ১৮ ms অপেক্ষা করে। এক Core-এ, কড়াভাবে একটার পর একটা চালালে আর অপেক্ষা একসাথে চালালে সর্বোচ্চ Throughput কত হবে? ধরে নাও Database পেরে ওঠে।
নিজেকে যাচাই করোপ্রশ্ন
তিনটা Job-এ প্রত্যেকটায় এক Core-এ ৩০ ms পুরো CPU লাগে। একটার পর একটার তুলনায় ১০ ms-এর Slice-এ চালালে কী হয় (Switch-এর খরচ বাদে)?
নিজেকে যাচাই করোপ্রশ্ন
এক Core-এ Concurrency সত্যিই কোন দুটো লাভ দিতে পারে?
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
Context Switch-এ নিচের কোনটা জমা হয় না?
নিজেকে যাচাই করোপ্রশ্ন
এক Process-এর দুই Thread-এর Switch-এর তুলনায় দুই আলাদা Process-এর Thread-এর Switch-এ বাড়তি কী লাগে?
নিজেকে যাচাই করোপ্রশ্ন
একটা Thread getpid() ডাকল, সেটা সাথে সাথে ফিরল, Thread চলতেই থাকল। কী ঘটল বলবে?
নিজেকে যাচাই করোপ্রশ্ন
একটা Thread Data ছাড়া Socket-এ recv() ডেকে ঘুমিয়ে পড়ল। এটা কীভাবে গোনা হয়?
নিজেকে যাচাই করোপ্রশ্ন
pidstat -w দেখাচ্ছে একটা Worker-এর ৯০০ nvcswch/s আর ৫ cswch/s, CPU বেশি। এটা কী বলে?
নিজেকে যাচাই করোপ্রশ্ন
একটা Service-এ cswch/s বেশি, CPU ২০%, Request ধীর। CPU বাড়িয়ে লাভ হয়নি। প্রথমে কোথায় দেখবে?
নিজেকে যাচাই করোপ্রশ্ন
আধুনিক Server-এ Context Switch-এর সরাসরি অংশে মোটামুটি কত লাগে?
নিজেকে যাচাই করোপ্রশ্ন
Context Switch-এর কোন দুটো খরচ পরোক্ষ, আর প্রায়ই সরাসরি খরচের চেয়ে বড়?
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
৮ Core-এর Server-এ vmstat-এ cs প্রায় ২,০০,০০০। প্রতি Switch-এ মোট প্রায় ২০ µs লাগলে মোটামুটি কতটা CPU Switch-এ যাচ্ছে?
নিজেকে যাচাই করোপ্রশ্ন
৪ Core-এ ১,০০০ CPU-bound Thread চলছে, Time Slice প্রায় ৩ ms। এক পালা থেকে পরের পালা পর্যন্ত একটা Thread মোটামুটি কতক্ষণ অপেক্ষা করে?
নিজেকে যাচাই করোপ্রশ্ন
একটা Message Router-এ ৫,০০০ Thread। প্রত্যেকে জাগে, ২০ µs-এ একটা Message সামলায়, আটকে যায়। CPU ১০০%, cs বিশাল, Throughput কম। প্রথম ভালো সমাধান কী?
নিজেকে যাচাই করোপ্রশ্ন
CFS পরের Thread কীভাবে বাছত?
নিজেকে যাচাই করোপ্রশ্ন
Kernel ৬.৬ থেকে Linux-এর ডিফল্ট EEVDF, CFS-এর তুলনায় নতুন কী আনে?
নিজেকে যাচাই করোপ্রশ্ন
nice 5-এর একটা Batch Job আর nice 0-র একটা Web Worker দুজনেই এক Core-এ ব্যস্ত। পরে Web Worker ফাঁকা হলো। দুই অবস্থায় Batch Job কতটা পায়?
নিজেকে যাচাই করোপ্রশ্ন
একটা Web Handler বেশিরভাগ সময় আটকে থাকে। ব্যস্ত Core-এ জাগলে সাধারণত দ্রুতই চলে। কেন?
নিজেকে যাচাই করোপ্রশ্ন
একটা কাজের ৯০% Parallel করা যায়। ৮ Core-এ Speedup কত, আর অসীম Core-এ সীমা কত?
নিজেকে যাচাই করোপ্রশ্ন
Server-এ Amdahl-এর সূত্র নিয়ে কোন দুটো কথা ঠিক?
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
Load Test-এ ১২ Core পর্যন্ত Throughput বাড়ে, ২৪ আর ৪৮ Core-এ পড়ে যায়। সাধারণ Amdahl বলে সবচেয়ে খারাপ হলেও সমান থাকবে। পড়ে কেন?
নিজেকে যাচাই করোপ্রশ্ন
১৬ vCPU-র একটা Instance-এ ৮টা আসল Core, Hyper-Threading চালু। ৮ Thread-এ একটা CPU-bound Job প্রায় ৮ গুণ পায়। ১৬ Thread-এ বাস্তবসম্মত ফল কী?
নিজেকে যাচাই করোপ্রশ্ন
একটা দল ১৬ Core-এর Machine-এ ডিফল্ট Build-এ পুরো Python-এ ছবি ছোট করতে Thread Pool ব্যবহার করছে। CPU-তে একটা Core ব্যস্ত দেখায়। কী করা উচিত?
নিজেকে যাচাই করোপ্রশ্ন
CPU-bound Python কাজে কোন দুটো উপায় সত্যিকারের Parallelism দিতে পারে?
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
একটা Kubernetes Service-এ limits.cpu: 2, গড় CPU সেই Limit-এর প্রায় ৪০%, p99-এ প্রায় ৮০ ms-এর লাফ। ৬৪ Core-এর Node-এ ১৬টা Worker Thread চলে। সবচেয়ে সম্ভাব্য কারণ কী?
নিজেকে যাচাই করোপ্রশ্ন
একটা Container-এর cgroup-এ cpu.max হলো 200000 100000। এর মানে কী?
নিজেকে যাচাই করোপ্রশ্ন
Kubernetes-এর CPU Request আর Limit নিয়ে কোন দুটো কথা ঠিক?
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
৪ Core-এর Machine-এ পুরোপুরি CPU-bound একটা Job সমান স্বাধীন টুকরোয় ভাগ হয়। কত Thread-এ সম্ভবত সবচেয়ে ভালো Throughput পাবে?
নিজেকে যাচাই করোপ্রশ্ন
একটা Program শুধু এক Core-এর Machine-এ দুই Thread থেকে Lock ছাড়া একটা Shared Counter বাড়ায়। Update হারাতে পারে?