এই পর্বে যা শিখবেন
- Concurrency বনাম parallelism, আর I/O-bound বনাম CPU-bound কাজ চেনা
threading.Thread-এর start/join, আর আধুনিকThreadPoolExecutor-এর map, submit, as_completed- Race condition কেন ঘটে,
Lockদিয়ে কীভাবে ঠেকাবেন, আরQueueদিয়ে নিরাপদ আদান-প্রদান - GIL কেন আছে, কী করে, আর CPU-ভারী কাজে
ProcessPoolExecutorকীভাবে সাহায্য করে - Free-threaded Python (PEP 703, PEP 779)-এর সঠিক অবস্থা, আর কোন কাজে কোন পদ্ধতি বেছে নেবেন
১ · একসাথে অনেক কাজ — concurrency বনাম parallelism
আগের পর্বে আপনি ডিবাগিং আর logging শিখেছেন — প্রোগ্রাম ঠিকমতো চলছে কি না, তা দেখার কৌশল। এবার প্রশ্ন বদলাচ্ছে: প্রোগ্রাম ঠিক চলছে, কিন্তু ধীর. ধরুন আপনার একটি স্ক্রিপ্ট ১০টি ওয়েবসাইট থেকে খবর আনে। প্রতিটি সাইট উত্তর দিতে এক সেকেন্ড নেয়। একটার পর একটা আনলে লাগে ১০ সেকেন্ড। অথচ এই ১০ সেকেন্ডের প্রায় পুরোটাই আপনার কম্পিউটার কিছুই করছে না — শুধু অপেক্ষা করছে, কখন উত্তর আসবে।
এই অপেক্ষার সময়টা কাজে লাগানোর বিদ্যাই এই পর্বের বিষয়। শুরুতে দুটি শব্দের পার্থক্য পরিষ্কার করা দরকার, কারণ অনেকে এদের একই মনে করেন।
🔀 Concurrency
অনেকগুলো কাজ একই সময়-পর্বে সামলানো. একটি কাজ অপেক্ষায় থাকলে অন্যটি এগিয়ে নেওয়া। একজন মানুষও এটা পারেন — একসাথে তিনটি কাজ ধরে রেখে পালা করে এগোনো।
কাঠামোর ব্যাপার⚡ Parallelism
একই মুহূর্তে একাধিক কাজ সত্যিই চলা। এর জন্য একাধিক কর্মী লাগে — কম্পিউটারে একাধিক CPU core. আজকের প্রায় সব ল্যাপটপ আর ফোনে ৪ থেকে ১৬টি core থাকে।
হার্ডওয়্যারের ব্যাপারপুরান ঢাকার একটি চায়ের দোকানের কথা ভাবুন। দোকানি একজনই। তিনি কেতলিতে পানি চড়িয়ে দিলেন; পানি ফোটার অপেক্ষায় না দাঁড়িয়ে কাপ ধুলেন, এক খদ্দেরের টাকা নিলেন, বিস্কুটের বয়াম খুললেন। পানি ফুটলে আবার চায়ে ফিরলেন। তিনি একসাথে চারটি কাজ সামলাচ্ছেন, কিন্তু কোনো এক মুহূর্তে তাঁর হাত একটিই কাজ করছে — এটা concurrency. এবার দোকানে তিনজন কর্মচারী এলেন; একজন চা বানান, একজন কাপ ধোন, একজন টাকা নেন — একই মুহূর্তে তিনটি কাজ। এটা parallelism.
কোন কাজ কীসের জন্য অপেক্ষা করে — I/O-bound বনাম CPU-bound
কোন পদ্ধতি কাজে আসবে, তা নির্ভর করে আপনার কাজটি কোথায় সময় খরচ করছে, তার ওপর। এটাই এই পর্বের সবচেয়ে গুরুত্বপূর্ণ প্রশ্ন।
| ধরন | সময় যায় কোথায় | উদাহরণ |
|---|---|---|
| I/O-bound | বাইরের কিছুর অপেক্ষায় — নেটওয়ার্ক, ডিস্ক, ডেটাবেস, ব্যবহারকারী | ১০টি ওয়েবপাতা নামানো, ৫টি মুদ্রার বিনিময় হার API থেকে আনা, ১০০টি ফাইল পড়া |
| CPU-bound | CPU নিজেই হিসাব করছে, বিশ্রাম নেই | ১ কোটি পর্যন্ত prime সংখ্যা গোনা, ছবির প্রতিটি pixel বদলানো, বড় লুপে গাণিতিক হিসাব |
I/O-bound কাজে CPU বসে থাকে, তাই একজন কর্মীও পালা করে অনেক কাজ এগিয়ে নিতে পারে — concurrency যথেষ্ট। CPU-bound কাজে CPU এক মুহূর্তও বসে নেই; এটা দ্রুত করতে হলে কাজ ভাগ করে একাধিক core-এ একসাথে চালাতে হয় — parallelism লাগে।
সাধারণ (ডিফল্ট) CPython-এ একটি process-এর ভেতরে একসাথে শুধু একটি thread Python bytecode চালাতে পারে — এর কারণ GIL, যা পর্ব ০৫-এ প্রথম দেখেছেন। ফলাফল: thread অপেক্ষার কাজে (I/O) দারুণ, কিন্তু ভারী হিসাবে (CPU) গতি বাড়ায় না। ভারী হিসাবের জন্য আলাদা process লাগে, অথবা Python 3.14-এর ঐচ্ছিক free-threaded build. পুরো পর্বটি এই এক বাক্যের বিস্তার।
২ · thread — একই প্রোগ্রামের ভেতরে একাধিক কর্মী
আপনি যখন python app.py চালান, operating system একটি process তৈরি করে — নিজস্ব মেমরি, নিজস্ব ফাইল-হ্যান্ডেল সহ একটি চলমান প্রোগ্রাম। শুরুতে তার ভেতরে একটিই কাজের ধারা থাকে, যাকে বলে main thread. একটি thread হলো process-এর ভেতরের একটি স্বাধীন কাজের ধারা। একই process-এর সব thread একই মেমরি ভাগ করে — একই ভ্যারিয়েবল, একই list, একই dict দেখে। এটাই thread-এর সুবিধা (ডেটা দেওয়া-নেওয়া সহজ), আবার বিপদও (একসাথে বদলালে গোলমাল, অংশ ৪-এ)।
Python-এর standard library-তে আছে threading module. সবচেয়ে মৌলিক ব্যবহার দেখুন — তিনটি পাতা “নামানো”-র কাজ তিনটি thread-এ:
import threading
def download(page, results):
results[page] = f"{page}: পাতা এসেছে" # আসলে নেটওয়ার্কের কাজ
pages = ["home", "news", "sports"]
results = {}
threads = [threading.Thread(target=download, args=(p, results)) for p in pages]
for t in threads:
t.start() # thread চালু — কাজ শুরু
for t in threads:
t.join() # শেষ হওয়া পর্যন্ত অপেক্ষা
for p in pages:
print(results[p])Thread(target=..., args=(...))— একটি thread object বানায়; কোন ফাংশন চলবে আর কী argument পাবে, তা বলে দেয়। এখনো কিছু চলেনি।t.start()— thread-টি সত্যিই চালু করে। এরপর main thread থেমে থাকে না, নিজের পরের লাইনে চলে যায়।t.join()— “এই thread শেষ না হওয়া পর্যন্ত আমি এখানে অপেক্ষা করব।” join ভুলে গেলে main thread হয়তো ফল ছাপতে চলে যাবে, অথচ কাজ তখনো শেষ হয়নি।- ফল ছাপা হলো শেষে, ক্রম মেনে। প্রতিটি thread নিজের ফল dict-এ রেখেছে; সবাই join হওয়ার পর main thread নির্দিষ্ট ক্রমে ছেপেছে। thread-এর ভেতর থেকে সরাসরি
printকরলে কোন লেখা আগে আসবে, তার কোনো নিশ্চয়তা থাকে না।
Thread-এর ক্রম অনুমান করবেন না। কোন thread আগে চলবে, কখন থামবে, তা ঠিক করে operating system আর Python-এর scheduler — আপনার কোডের লাইনের ক্রম নয়। একই প্রোগ্রাম আজ এক ক্রমে, কাল অন্য ক্রমে চলতে পারে। তাই ভালো নকশার নিয়ম: প্রতিটি thread ফল জমা রাখুক, আর সব শেষে এক জায়গা থেকে ফল পড়ুন।
Daemon thread — পেছনের সহকারী
সাধারণত Python প্রোগ্রাম শেষ হওয়ার আগে সব চালু thread শেষ হওয়ার অপেক্ষা করে। কিন্তু কিছু thread চিরকাল চলার জন্য বানানো — যেমন প্রতি মিনিটে log ফাইলে স্বাস্থ্য-তথ্য লেখা। এমন thread-কে Thread(target=..., daemon=True) দিয়ে daemon thread বানানো যায়। Main thread শেষ হলে daemon thread-গুলোকে আর অপেক্ষা না করে হঠাৎ থামিয়ে দেওয়া হয় — তাদের finally ব্লক বা ফাইল বন্ধ করার কাজ না-ও চলতে পারে। তাই ফাইলে লেখা বা ডেটাবেসে সেভ করার মতো গুরুত্বপূর্ণ কাজ কখনো daemon thread-এ রাখবেন না।
৩ · ThreadPoolExecutor — আধুনিক, সহজ পথ
প্রতিটি কাজের জন্য হাতে thread বানানো, start আর join করা ঝামেলার; ১০০০টি পাতা নামাতে ১০০০টি thread বানালে অপচয়ও। বাস্তবে প্রায় সবসময় ভালো উপায় হলো thread pool: নির্দিষ্ট কয়েকজন কর্মী আগে থেকে তৈরি থাকে, কাজগুলো একটি সারিতে জমা হয়, যে কর্মী খালি হয় সে পরের কাজ তুলে নেয়। Python 3.2 থেকে standard library-র concurrent.futures module এই কাজ সহজ করেছে (PEP 3148 অনুযায়ী)। এর executor দুই রকম — ThreadPoolExecutor আর ProcessPoolExecutor — আর দুটির ব্যবহার প্রায় একই।
নিচের ডায়াগ্রামে দেখুন, ৩ জন কর্মী কীভাবে ৬টি মুদ্রার হার আনার কাজ ভাগ করে নেয়, আর ফল কোন ক্রমে ফেরত আসে:
1codes = ["USD", "EUR", "GBP", "INR", "SAR", "JPY"]2with ThreadPoolExecutor(max_workers=3) as pool:3 for code, rate in pool.map(fetch_rate, codes):4 print(code, rate)
| কর্মী ১ | |
| কর্মী ২ | |
| কর্মী ৩ | |
| সারিতে বাকি | |
| তৈরি ফল |
- Pool তৈরি।
withব্লকে ঢুকতেই একটি pool তৈরি হলো, যাতে সর্বোচ্চ ৩ জন কর্মী-thread থাকবে। কাজ জমা পড়লে কর্মীরা তৈরি হয়, আর তারপর বারবার ব্যবহার হয় — প্রতিটি কাজের জন্য নতুন thread নয়। ৬টি কাজ এখনো দেওয়া হয়নি। - প্রথম তিনটি কাজ তিনজনের হাতে।
pool.mapসব কাজ সারিতে জমা দিল। কর্মী ১, ২, ৩ যথাক্রমে USD, EUR, GBP তুলে নিল। তিনজনই এখন নেটওয়ার্কের অপেক্ষায় — আর অপেক্ষার সময় তারা GIL ছেড়ে দেয়, তাই তিনটি অপেক্ষা সত্যিই একসাথে চলছে। - EUR আগে শেষ — কিন্তু আগে ছাপা হয় না। EUR-এর সার্ভার দ্রুত উত্তর দিল। কর্মী ২ খালি হয়ে সারি থেকে INR তুলে নিল। কিন্তু
mapকথা দিয়েছে ফল দেবে ইনপুটের ক্রমে, তাই সে প্রথম ফল USD-এর জন্য অপেক্ষা করছে। EUR-এর ফল তৈরি হয়ে জমা আছে। - USD আর GBP এল — তিনটি ফল একসাথে বের হলো। USD আসতেই map প্রথম ফল দিল, তারপর জমে থাকা EUR, তারপর GBP. খালি হওয়া কর্মী ১ আর ৩ বাকি দুটি কাজ (SAR, JPY) নিল। কোনো কর্মী বসে নেই, সারি খালি।
- সব শেষ, ক্রম অটুট। সব ৬টি ফল এসেছে ঠিক
codes-এর ক্রমে, যদিও কাজগুলো শেষ হয়েছে অন্য ক্রমে।withব্লক শেষ হলে pool সব কর্মীর কাজ শেষ হওয়ার অপেক্ষা করে, তারপর বন্ধ হয় — আলাদা করে join লিখতে হয় না।
Executor-এর তিনটি মূল হাতিয়ার:
🗺️ map(fn, items)
Built-in map-এর মতোই, কিন্তু কাজগুলো কর্মীদের মধ্যে ভাগ হয়। ফল আসে ইনপুটের ক্রমে. সবচেয়ে সহজ, আর বেশিরভাগ সময় এটাই যথেষ্ট।
📨 submit(fn, *args)
একটি কাজ জমা দিয়ে সঙ্গে সঙ্গে একটি Future ফেরত দেয় — “ভবিষ্যতে ফল আসবে” এমন একটি রসিদ। পরে future.result() ডাকলে ফল পান (দরকার হলে অপেক্ষা করে)।
🏁 as_completed(futures)
Future-গুলো যে ক্রমে শেষ হয়, সেই ক্রমে দেয়। প্রথম ফল আসামাত্র কাজ শুরু করতে চাইলে (যেমন progress দেখানো) এটি কাজের। ক্রম প্রতিবার আলাদা হতে পারে।
যে আগে শেষ, সে আগেএবার কোডে দেখুন। এখানে আসল নেটওয়ার্কের বদলে একটি কাল্পনিক “API” (একটি dict) আর খুব ছোট time.sleep দিয়ে অপেক্ষার ভান করা হয়েছে। হারগুলোও কাল্পনিক:
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
FAKE_API = {"USD": 122.5, "EUR": 133.1, "GBP": 155.8, "INR": 1.45, "SAR": 32.6}
def fetch_rate(code):
time.sleep(0.1) # নেটওয়ার্কের অপেক্ষার ভান
return code, FAKE_API[code]
codes = ["USD", "EUR", "GBP", "INR", "SAR"]
with ThreadPoolExecutor(max_workers=5) as pool:
results = list(pool.map(fetch_rate, codes)) # ফল আসে ইনপুটের ক্রমে
futures = [pool.submit(fetch_rate, c) for c in codes]
rates = dict(f.result() for f in as_completed(futures))
for code, rate in results:
print(f"1 {code} = {rate} টাকা")
print(f"as_completed: {len(rates)}টি ফল, সবচেয়ে দামি {max(rates, key=rates.get)}")প্রথম পাঁচ লাইনের আউটপুট সবসময় USD, EUR, GBP, INR, SAR — এই ক্রমে, কারণ pool.map ইনপুটের ক্রম রক্ষা করে। পাঁচটি কাজ পাঁচজন কর্মীর হাতে একসাথে গেছে, তাই পাঁচটি ০.১ সেকেন্ডের অপেক্ষা মিলে মোট সময় প্রায় ০.১ সেকেন্ড, ০.৫ নয়। (সময় আমরা ছাপিনি, কারণ তা প্রতিবার একটু আলাদা হয়।) দ্বিতীয় অংশে submit দিয়ে Future বানিয়ে as_completed দিয়ে ফল নেওয়া হয়েছে — সেগুলো কোন ক্রমে এসেছে তা অনিশ্চিত, তাই আমরা সরাসরি ছাপিনি; dict-এ জমিয়ে শুধু সংখ্যা আর সবচেয়ে বড় হার (GBP) ছেপেছি, যা ক্রমের ওপর নির্ভর করে না। কোনো কাজ exception তুললে f.result() ডাকার সময় সেই exception আবার তোলা হয় — তাই ভুল চাপা পড়ে না।
কতজন কর্মী? max_workers না দিলে Python 3.8 থেকে ThreadPoolExecutor নিজে বেছে নেয় min(32, CPU সংখ্যা + 4). I/O-bound কাজে এর বেশিও হতে পারে, তবে অসীম নয় — যে সার্ভার থেকে আনছেন, তার ওপর একসাথে শত শত অনুরোধ পাঠানো অভদ্রতা, অনেক সাইট তা আটকেও দেয় (পর্ব ৪৭-এ দায়িত্বের সাথে scraping নিয়ে কথা হবে)।
৪ · ভাগ করা ডেটা আর race condition — Lock
এতক্ষণ প্রতিটি thread নিজের আলাদা ফল রেখেছে। এবার ধরুন একটি ব্যাংকের শাখায় চারটি কাউন্টার, আর চারজন ক্যাশিয়ার একই অ্যাকাউন্টে টাকা জমা দিচ্ছেন। চারটি thread, একটি ভাগ করা ভ্যারিয়েবল balance. প্রত্যেকে লেখেন balance += 100. এক লাইনের কাজ — কী আর ভুল হতে পারে?
সমস্যা হলো, balance += 100 দেখতে এক কাজ হলেও Python-এর virtual machine-এর কাছে কয়েকটি আলাদা bytecode নির্দেশ (পর্ব ০৫ মনে করুন): মান পড়া, ১০০ যোগ, ফল লেখা। একে বলে read-modify-write. GIL একবারে একটি নির্দেশ সুরক্ষিত রাখে, পুরো লাইন নয় — দুটি নির্দেশের মাঝখানে Python অন্য thread-কে পালা দিতে পারে। তখন কী হয়, দেখুন:
1T1: পড়ো balance2 ── Python পালা দিল T2-কে ──3T2: পড়ো balance4T2: যোগ + 100, লেখো5T1: যোগ + 1006T1: লেখো balance7# Lock থাকলে: T1-এর তিন ধাপ শেষ, তারপর T2
| balance | |
| T1-এর হাতে | |
| T2-এর হাতে |
- শুরু। অ্যাকাউন্টে 500 টাকা। দুটি thread (দুই ক্যাশিয়ার)
balance += 100চালাবে। ঠিক ফল হওয়া উচিত 700. - T1 পড়ল। T1 মান পড়ে নিজের হাতে (VM-এর stack-এ) 500 রাখল। এখনো যোগ বা লেখা হয়নি।
- পালা বদল — বিপদের বীজ। ঠিক এই মুহূর্তে Python সিদ্ধান্ত নিল T1 যথেষ্ট সময় পেয়েছে, এবার T2-এর পালা। T1 থেমে গেল, কিন্তু তার হাতে পুরোনো 500 রয়ে গেল। GIL এখানে কিছু আটকায় না — সে শুধু নিশ্চিত করে একসাথে দুজন bytecode চালাচ্ছে না।
- T2 পড়ল। T1 এখনো লেখেনি, তাই T2-ও দেখল 500.
- T2 যোগ করে লিখল। balance এখন 600. এ পর্যন্ত সব ঠিক মনে হচ্ছে।
- T1 জেগে উঠল — পুরোনো ছবি নিয়ে। T1 আবার পালা পেল, আর ঠিক যেখানে থেমেছিল সেখান থেকে চলল: তার হাতের 500-এর সাথে 100 যোগ করে 600. সে জানে না এর মধ্যে balance বদলে গেছে।
- T1 লিখল — একটি জমা হারিয়ে গেল। T1 তার 600 লিখে T2-এর লেখার ওপর চাপিয়ে দিল। দুবার জমা হলো, balance বাড়ল একবার। একে বলে lost update, আর যে পরিস্থিতিতে ফল নির্ভর করে কে আগে চলল তার ওপর, তাকে বলে race condition.
- তালা থাকলে।
with lock:দিয়ে তিনটি ধাপ ঘিরে দিলে T1 পড়া-যোগ-লেখা শেষ না করা পর্যন্ত T2 ভেতরে ঢুকতে পারে না। পালা বদল হলেও T2 তালার সামনে অপেক্ষা করে। T2 পড়ে 600, লেখে 700 — ঠিক।
“আমি চালিয়ে দেখলাম, ঠিকই তো আসে” — এটা প্রমাণ নয়। সাম্প্রতিক CPython-এ পালা বদল সাধারণত লুপের শেষে বা ফাংশন ডাকার মতো কিছু নির্দিষ্ট জায়গায় হয়, তাই সাধারণ build-এ এই ভুল হয়তো হাজারবার চালিয়েও দেখবেন না। কিন্তু ভাষা এমন কোনো নিশ্চয়তা দেয় না; অন্য version, অন্য যন্ত্র, বা free-threaded build-এ (যেখানে thread সত্যিই একসাথে চলে) একই কোড নিয়মিত টাকা “হারাবে”। Race condition-এর ভয়ংকর দিক এটাই — পরীক্ষায় লুকিয়ে থাকে, গ্রাহকের সামনে প্রকাশ পায়।
সমাধান: threading.Lock আর with lock:
সমাধান একটি তালা — Lock. কোডের যে অংশ ভাগ করা ডেটা পড়ে-বদলায়, তাকে বলে critical section. তার চারপাশে তালা দিন: একজন তালা ধরে থাকলে বাকিরা অপেক্ষা করে। Lock একটি context manager, তাই with lock: লিখলে ঢোকার সময় তালা লাগে, বের হওয়ার সময় — exception এলেও — নিশ্চিতভাবে খোলে। with-এর ভেতরের এই কৌশল পর্ব ৩৬-এ (context manager) দেখেছেন।
import threading
balance = 0
lock = threading.Lock()
def deposit(times):
global balance
for _ in range(times):
with lock: # একবারে একটিই thread ঢুকতে পারে
balance += 100
workers = [threading.Thread(target=deposit, args=(10_000,)) for _ in range(4)]
for t in workers:
t.start()
for t in workers:
t.join() # সবাই শেষ না হওয়া পর্যন্ত অপেক্ষা
print("মোট জমা:", balance)
print("প্রত্যাশিত:", 4 * 10_000 * 100)চারটি thread প্রত্যেকে ১০,০০০ বার ১০০ টাকা জমা দিল। প্রতিটি balance += 100 তালার ভেতরে, তাই পড়া-যোগ-লেখার মাঝখানে অন্য কেউ ঢুকতে পারেনি। ফল প্রতিবার ঠিক 4000000, প্রত্যাশিত মানের সমান। খেয়াল করুন, দুটি জিনিস একসাথে লাগে: তালা (মাঝপথে কেউ যেন না ঢোকে) আর join (সবাই শেষ হওয়ার আগে যেন ফল না পড়ি)। যেকোনো একটি বাদ দিলে ফল আর নির্ভরযোগ্য থাকে না। এখানে তালা প্রতিবার লাগানো-খোলায় কিছু খরচ আছে; বাস্তবে প্রতিটি thread নিজের স্থানীয় যোগফল রেখে শেষে একবার তালা দিয়ে মূল হিসাবে যোগ করলে আরও দ্রুত হয়।
Deadlock — দুজনেই অপেক্ষায়, কেউ এগোয় না
তালা সমস্যা মেটায়, আবার নতুন সমস্যাও আনে। ধরুন thread A আগে “হিসাবের খাতা”-র তালা নিল, তারপর “ক্যাশ বাক্স”-র তালা চাইছে। একই সময়ে thread B আগে ক্যাশ বাক্সের তালা নিয়ে এখন খাতার তালা চাইছে। দুজনেই অন্যজনের হাতের তালার জন্য অপেক্ষা করছে — চিরকাল। একে বলে deadlock: প্রোগ্রাম crash করে না, কোনো error দেখায় না, শুধু থেমে থাকে। প্রতিরোধের সবচেয়ে কার্যকর নিয়ম: একাধিক তালা লাগলে সব জায়গায় একই ক্রমে নিন; তালার ভেতরে যতটা সম্ভব কম কাজ করুন (বিশেষ করে নেটওয়ার্কের অপেক্ষা নয়); আর সম্ভব হলে তালা ভাগাভাগির বদলে পরের অংশের Queue ব্যবহার করুন। একই thread নিজে একটি সাধারণ Lock দুবার নিতে গেলেও আটকে যায় — সেই প্রয়োজনে আছে threading.RLock.
৫ · queue.Queue — তালা ছাড়াই নিরাপদ আদান-প্রদান
তালা নিয়ে কাজ করা কঠিন, কারণ প্রতিটি ভাগ করা ভ্যারিয়েবল মনে রাখতে হয়। অনেক বেশি পরিষ্কার নকশা হলো: thread-রা ডেটা ভাগ করবে না, বরং একে অপরকে বার্তা পাঠাবে. এর জন্য Python দিয়েছে queue.Queue — একটি thread-safe সারি, যার ভেতরে তালার কাজ আগে থেকেই করা আছে। একদিকে কেউ put() করে, অন্যদিকে কেউ get() করে।
ধানমন্ডির একটি রেস্টুরেন্ট। ওয়েটার অর্ডার নিয়ে রান্নাঘরের জানালার তাকে স্লিপ রাখেন; বাবুর্চি তাক থেকে একটা একটা স্লিপ তুলে রান্না করেন। ওয়েটার আর বাবুর্চি কখনো একই কাগজে একসাথে হাত দেন না — তাকটাই তাদের যোগাযোগের একমাত্র পথ। তাক খালি থাকলে বাবুর্চি অপেক্ষা করেন; তাক ভরে গেলে ওয়েটার নতুন স্লিপ রাখার জায়গার জন্য অপেক্ষা করেন। এই নকশার নাম producer–consumer: ওয়েটার producer, বাবুর্চি consumer, তাক হলো queue.
import queue
import threading
orders = queue.Queue(maxsize=2) # তাকে একসাথে সর্বোচ্চ ২টি অর্ডার
cooked = []
def chef():
while True:
dish = orders.get() # খালি থাকলে অপেক্ষা
if dish is None: # sentinel: আজকের মতো শেষ
break
cooked.append(f"{dish} তৈরি")
orders.task_done()
cook = threading.Thread(target=chef)
cook.start()
for dish in ["বিরিয়ানি", "খিচুড়ি", "চা", "কাবাব"]:
orders.put(dish) # তাক ভরা থাকলে অপেক্ষা
orders.join() # সব অর্ডার শেষ হওয়া পর্যন্ত
orders.put(None)
cook.join()
print(cooked)['বিরিয়ানি তৈরি', 'খিচুড়ি তৈরি', 'চা তৈরি', 'কাবাব তৈরি']
বাবুর্চি একজন, তাই অর্ডার যে ক্রমে তাকে উঠেছে সেই ক্রমেই রান্না হয়েছে — আউটপুট সবসময় একই। হাতে-কলমে ধাপগুলো দেখুন:
1বাবুর্চি: dish = orders.get()2ওয়েটার: orders.put(dish)3ওয়েটার: put — তাক ভরা, অপেক্ষা4বাবুর্চি: task_done(), আবার get()5ওয়েটার: join(), তারপর put(None)
| তাক | |
| ওয়েটার | |
| বাবুর্চি |
- বাবুর্চি অপেক্ষায়। বাবুর্চি-thread চালু হয়ে
orders.get()ডাকলেন, কিন্তু তাক খালি। তিনি ঘুমিয়ে পড়লেন — CPU খরচ না করে, বারবার উঁকি না দিয়ে। Queue নিজেই তাঁকে জাগাবে। - প্রথম অর্ডার। ওয়েটার
put("বিরিয়ানি")করলেন। Queue বাবুর্চিকে জাগাল, তিনি স্লিপটি তুলে নিলেন। কোনো তালা আমরা লিখিনি — Queue-র ভেতরে তালা আর সংকেত দুটোই আছে। - তাক ভরে গেল। বাবুর্চি রান্নায় ব্যস্ত, এর মধ্যে ওয়েটার আরও দুটি অর্ডার রাখলেন।
maxsize=2, তাই তাক এখন ভরা। - ওয়েটার থামলেন — এটা ভালো জিনিস। চতুর্থ অর্ডার (কাবাব) রাখতে গিয়ে
put()আটকে গেল। দ্রুত producer ধীর consumer-কে ডুবিয়ে দিতে পারে না; মেমরিতে অসীম অর্ডার জমে না। একে বলে backpressure. - জায়গা হলো, কাজ এগোল। বাবুর্চি বিরিয়ানি শেষ করে
task_done()জানালেন আর পরের স্লিপ (খিচুড়ি) তুললেন। তাকে জায়গা হতেই ওয়েটারের আটকে থাকাputসফল হলো। - দোকান বন্ধ। ওয়েটার
orders.join()-এ অপেক্ষা করলেন যতক্ষণ না প্রতিটি অর্ডারেরtask_done()আসে। তারপর একটি বিশেষ চিহ্নNone(sentinel) রাখলেন। বাবুর্চি সেটি পেয়ে লুপ থেকে বের হলেন, main thread তাঁকে join করল। ফল: চারটি পদ, ঠিক ক্রমে।
একাধিক বাবুর্চি (consumer) রাখলে রান্না দ্রুত হবে, তবে কোন পদ কে আগে শেষ করবে তা আর নির্দিষ্ট থাকবে না — তখন প্রতিটি বাবুর্চির জন্য একটি করে None পাঠাতে হয়। Python 3.13 থেকে Queue.shutdown() নামে একটি method-ও আছে, যা sentinel ছাড়াই queue বন্ধ করার পরিষ্কার উপায় দেয়।
৬ · GIL — ভেতরের গল্প
এবার সেই বিখ্যাত তালার কথা, যার নাম আপনি পর্ব ০৫-এ প্রথম শুনেছেন। GIL (Global Interpreter Lock) হলো সাধারণ CPython-এর ভেতরের একটি তালা, যা একটি process-এ একসাথে শুধু একটি thread-কে Python bytecode চালাতে দেয়। যে thread GIL ধরে আছে, শুধু সে-ই Python object ছুঁতে পারে।
GIL কেন আছে
পর্ব ০৫-এ দেখেছেন, CPython প্রতিটি object-এর একটি reference count রাখে, আর গণনা শূন্য হলেই object মুছে দেয়। দুটি thread একই মুহূর্তে একই object-এর গণনা বাড়ালে-কমালে ঠিক উপরের balance-এর মতো lost update হতো — আর ফল টাকা হারানো নয়, ভুল সময়ে object মুছে যাওয়া, মানে interpreter crash. প্রতিটি object-এ আলাদা তালা দিলে প্রতিটি গণনা বদলে বাড়তি খরচ হতো, আর এক-thread-এর প্রোগ্রাম (যা ছিল প্রায় সব প্রোগ্রাম) ধীর হয়ে যেত।
তাই ১৯৯২ সালে Python-এ thread যোগ করার সময় Guido van Rossum বেছে নেন সরল সমাধান: পুরো interpreter-এর জন্য একটি বড় তালা। এর বড় লাভ ছিল C extension লেখকদের জন্য — তাঁদের নিজের কোডে thread-নিরাপত্তা নিয়ে প্রায় ভাবতেই হতো না, কারণ GIL ধরে থাকা অবস্থায় আর কেউ Python object ছোঁয় না। এই সরলতাই NumPy থেকে ডেটাবেস driver পর্যন্ত বিশাল C library-জগৎ গড়ে উঠতে সাহায্য করেছে।
GIL কখন হাতবদল হয়
দুটি উপায়ে একটি thread GIL ছেড়ে দেয়। এক, অপেক্ষার আগে: নেটওয়ার্ক থেকে পড়া, ফাইল পড়া, time.sleep — এমন blocking কাজের আগে CPython নিজেই GIL ছেড়ে দেয়, অপেক্ষা শেষে আবার নেয়। দুই, সময় ফুরালে: অন্য কোনো thread GIL চেয়ে অপেক্ষায় থাকলে, চলমান thread একটি নির্দিষ্ট সময় পর ছেড়ে দিতে বাধ্য হয়। এই সময়কে বলে switch interval, ডিফল্ট ৫ মিলিসেকেন্ড (sys.getswitchinterval() দিয়ে দেখা যায়)। দুই রকম thread-এর একটি দৃশ্য দেখুন:
1T1 (download): req = build_request(url)2T1 (download): data = sock.recv(4096)3T2 (হিসাব): total += n * n # লুপে4# দুটোই হিসাবের thread হলে?
| GIL | |
| T1 | |
| T2 | |
| ব্যস্ত core |
- T1 GIL ধরে আছে। T1 Python কোডে নেটওয়ার্ক অনুরোধ তৈরি করছে। T2 প্রস্তুত, কিন্তু GIL না পাওয়া পর্যন্ত একটি bytecode-ও চালাতে পারে না।
- T1 নেটওয়ার্কের অপেক্ষায় — GIL ছেড়ে দিল।
sock.recv()একটি blocking কাজ; উত্তর আসতে হয়তো ২০০ মিলিসেকেন্ড লাগবে। অপেক্ষা শুরুর আগেই CPython GIL ছেড়ে দিল। T2 GIL পেয়ে হিসাব শুরু করল। এখন T1-এর অপেক্ষা (নেটওয়ার্ক কার্ড আর operating system করছে) আর T2-এর হিসাব সত্যিই একসাথে চলছে — I/O-bound কাজে thread কেন কাজে আসে, তার পুরো রহস্য এটাই। - ডেটা এল, T1 GIL চাইছে। নেটওয়ার্ক থেকে উত্তর এসেছে, T1 এখন Python কোডে ডেটা সাজাতে চায়। সে GIL চেয়ে একটি অনুরোধ-চিহ্ন বসিয়ে অপেক্ষা করছে।
- Switch interval পার — হাতবদল। T2 টানা হিসাব করছে, নিজে থেকে ছাড়বে না। কিন্তু অনুরোধ-চিহ্ন দেখে প্রায় ৫ মিলিসেকেন্ড পর পরের নিরাপদ বিন্দুতে সে GIL ছেড়ে দিল। T1 ডেটা সাজাচ্ছে, T2 অপেক্ষায়।
- দুটোই হিসাবের thread হলে? এবার কেউ অপেক্ষা করে না, দুজনেই শুধু GIL চায়। তারা প্রতি ৫ মিলিসেকেন্ডে পালা বদলায়, কিন্তু যেকোনো মুহূর্তে একটিই core কাজ করে। ৮ core-এর মেশিনেও মোট সময় একটার পর একটা চালানোর সমান — আর হাতবদলের খরচের কারণে কখনো একটু বেশি। CPU-bound কাজে thread কেন কাজে আসে না, তার পুরো রহস্য এটাই।
CPU-bound + thread (GIL সহ) → পালা করে, দ্রুত নয়
একটি গুরুত্বপূর্ণ ব্যতিক্রম। C-তে লেখা অনেক library ভারী কাজের সময় নিজেই GIL ছেড়ে দেয় — যেমন NumPy-র বড় array-এর অনেক হিসাব, hashlib দিয়ে বড় ডেটার hash, zlib দিয়ে compression. তখন ভেতরের C কোড অন্য thread-এর সাথে সত্যিই একসাথে একাধিক core-এ চলতে পারে। তাই “thread মানেই CPU-তে লাভ নেই” কথাটি পুরোপুরি ঠিক নয় — সঠিক কথা হলো, বিশুদ্ধ Python কোডের হিসাবে লাভ নেই।
৭ · process — CPU-ভারী কাজের আসল সমাধান
প্রতিটি process-এর নিজস্ব Python interpreter, নিজস্ব মেমরি — আর নিজস্ব GIL. তাই ৪টি process মানে ৪টি আলাদা GIL, যারা ৪টি core-এ একসাথে Python bytecode চালাতে পারে। Standard library-র multiprocessing module এই কাজ করে, আর তার আধুনিক, সহজ রূপ হলো ProcessPoolExecutor — যার ব্যবহার ThreadPoolExecutor-এর হুবহু মতো।
উদাহরণ: ১ লাখ পর্যন্ত কতগুলো prime সংখ্যা আছে, তা চারটি ভাগে চারটি process-এ গোনা। এই কোডটি নিজের কম্পিউটারে ফাইল বানিয়ে চালান:
from concurrent.futures import ProcessPoolExecutor
def count_primes(bounds):
lo, hi = bounds
count = 0
for n in range(max(lo, 2), hi):
if all(n % d for d in range(2, int(n ** 0.5) + 1)):
count += 1
return count
if __name__ == "__main__":
ranges = [(0, 25_000), (25_000, 50_000), (50_000, 75_000), (75_000, 100_000)]
with ProcessPoolExecutor(max_workers=4) as pool:
counts = list(pool.map(count_primes, ranges))
for (lo, hi), c in zip(ranges, counts):
print(f"{lo}–{hi}: {c}টি prime")
print("মোট:", sum(counts))প্রত্যাশিত আউটপুট (যেকোনো যন্ত্রে একই):
0–25000: 2762টি prime 25000–50000: 2371টি prime 50000–75000: 2260টি prime 75000–100000: 2199টি prime মোট: 9592
৪ core-এর মেশিনে এটি এক process-এ গোনার চেয়ে উল্লেখযোগ্যভাবে দ্রুত হয় (ঠিক ৪ গুণ নয় — কেন, তা নিচে আর ভাবনার প্রশ্নে দেখবেন)। একই কোডে শুধু ProcessPoolExecutor-কে ThreadPoolExecutor বানালে সাধারণ CPython-এ প্রায় কোনো লাভ হবে না — নিজে বদলে মেপে দেখুন। তিনটি জিনিস অবশ্যই মনে রাখতে হবে:
১. if __name__ == "__main__": বাধ্যতামূলক
নতুন process তৈরির কয়েকটি পদ্ধতি আছে। Windows আর macOS-এ ডিফল্ট পদ্ধতি spawn: একটি একদম নতুন Python interpreter চালু হয়, আর আপনার মূল ফাইলটি module হিসেবে import করে count_primes খুঁজে নেয়। পর্ব ২২-এ (module আর import) দেখেছেন, import মানে ফাইলের উপরের স্তরের সব কোড চলা। Guard না থাকলে প্রতিটি নতুন process আবার pool বানাতে চাইত, সে আবার নতুন process — তাই Python এটা আটকে দিয়ে RuntimeError দেখায়। main guard-এর ভেতরের কোড শুধু আসল মূল process-এ চলে, import-এর সময় নয়। Linux-এ আগে ডিফল্ট ছিল fork (চলমান process-এর হুবহু কপি), যেখানে guard ছাড়াও কোড চলত — তাই Linux-এ লেখা কোড Windows-এ গিয়ে ভাঙত। Python 3.14 থেকে Linux-এও ডিফল্ট বদলে forkserver হয়েছে, যেখানে guard আবার জরুরি। সহজ নিয়ম: process ব্যবহার করলে সবসময় main guard.
২. Argument আর ফল pickle হয়ে যাতায়াত করে
Process-রা মেমরি ভাগ করে না। তাই count_primes-এ যে tuple পাঠালেন, তা pickle করে (byte-এ বদলে) অন্য process-এ পাঠানো হয়, সেখানে আবার object বানানো হয়; ফলও একইভাবে ফেরত আসে। এর তিনটি ফল আছে: lambda বা অন্য ফাংশনের ভেতরে লেখা ফাংশন সাধারণত পাঠানো যায় না (pickle হয় না) — ফাংশন হতে হবে module-এর উপরের স্তরে; বড় ডেটা (ধরুন ২ GB-র list) পাঠালে কপির খরচ বিশাল; আর worker process-এ কোনো global ভ্যারিয়েবল বদলালে মূল process তা দেখে না।
৩. Process দামি
একটি thread তৈরি হয় মাইক্রোসেকেন্ডে; একটি process (বিশেষ করে spawn-এ) চালু হতে interpreter লোড করতে হয়, যাতে অনেক বেশি সময় আর মেমরি লাগে। তাই process তখনই লাভজনক, যখন প্রতিটি কাজ যথেষ্ট বড় — হাজারটা ছোট কাজের জন্য হাজারটা পাঠানোর বদলে কয়েকটি বড় খণ্ডে ভাগ করুন (উপরে যেমন ৪টি পরিসর), অথবা pool.map(fn, items, chunksize=100) ব্যবহার করুন।
C++-এর std::thread সত্যিই একাধিক core-এ একসাথে চলে, তাই সেখানে CPU-ভারী কাজও thread দিয়ে ভাগ করা হয়, আর তালা ছাড়া একই ভ্যারিয়েবল বদলানো (data race) undefined behavior. সাধারণ CPython-এ GIL bytecode-কে পালা করে চালায়, তাই interpreter crash হয় না — কিন্তু balance += 100-এর মতো যুক্তির স্তরের race ঠিকই থাকে। Python-এর threading.Lock আর with lock: প্রায় C++-এর std::mutex আর std::scoped_lock-এর মতো, আর Queue-র ভেতরে আছে condition variable. বিস্তারিত তুলনার জন্য C++ কোর্সের multithreading পর্ব দেখুন।
৮ · Free-threaded Python — GIL ছাড়া CPython
বহু বছর ধরে “Python-এ সত্যিকারের parallel thread নেই” কথাটি প্রায় প্রবাদ ছিল। এখন তা বদলাচ্ছে, তবে ধাপে ধাপে, সাবধানে। সঠিক তথ্যগুলো একটু মনোযোগ দিয়ে পড়ুন, কারণ ইন্টারনেটে (আর AI-এর উত্তরে) এ নিয়ে অনেক ভুল আছে।
| কখন | কী ঘটল |
|---|---|
| ২০২৩ | Sam Gross-এর প্রস্তাব PEP 703 (“Making the Global Interpreter Lock Optional in CPython”) Python Steering Council গ্রহণ করে — ধাপে ধাপে, আলাদা build হিসেবে আনার শর্তে। |
| Python 3.13 (২০২৪) | প্রথমবার আলাদা একটি পরীক্ষামূলক free-threaded build. ডিফল্ট Python-এ GIL আগের মতোই। |
| Python 3.14 (২০২৫) | PEP 779 অনুযায়ী free-threaded build এখন অফিসিয়ালি সমর্থিত — আর “পরীক্ষামূলক” নয় — কিন্তু ঐচ্ছিক. আলাদা করে ইনস্টল করতে হয়; ডিফল্ট Python-এ GIL এখনো আছে। |
| ভবিষ্যৎ | কোনো দিন free-threaded build ডিফল্ট হবে কি না, তা এখনো ঠিক হয়নি; নির্ভর করছে library-গুলোর প্রস্তুতি আর গতির হিসাবের ওপর। |
Free-threaded build সাধারণত আলাদা নামের একটি executable — যেমন python3.14t (শেষের t দিয়ে threaded বোঝানো হয়)। python.org-এর Windows ও macOS installer-এ এটি একটি ঐচ্ছিক অংশ হিসেবে বেছে নেওয়া যায়, আর uv-এর মতো টুল দিয়েও ইনস্টল করা যায়। আপনি কোনটি চালাচ্ছেন, তা যাচাই করতে পারেন:
$ python3.14t -VV
Python 3.14.0 free-threading build (...)
$ python3.14t -c "import sys; print(sys._is_gil_enabled())"
Falseএই build-এ উপরের prime-গোনার কোডে শুধু ThreadPoolExecutor ব্যবহার করলেই চারটি thread চারটি core-এ সত্যিই একসাথে চলতে পারে — process-এর pickle বা চালুর খরচ ছাড়া। কিন্তু কয়েকটি কথা জেনে রাখুন:
- C extension-কে সমর্থন জানাতে হয়। GIL না থাকলে C extension-গুলোকে নিজের thread-নিরাপত্তা নিজেকেই নিশ্চিত করতে হয়, আর সেই অনুযায়ী আলাদা করে build করতে হয়। যে extension নিজেকে free-threading-এর উপযুক্ত বলে ঘোষণা করেনি, তা import করলে interpreter নিরাপত্তার জন্য সাধারণত GIL আবার চালু করে দেয় এবং একটি সতর্কবার্তা দেখায়। NumPy-সহ জনপ্রিয় অনেক library ইতোমধ্যে সমর্থন যোগ করেছে, কিন্তু সব নয় — আপনার প্রয়োজনীয় library-গুলো আগে যাচাই করুন।
- এক-thread-এর কোডে সামান্য খরচ। Thread-নিরাপদ reference counting আর মেমরি ব্যবস্থার কারণে free-threaded build-এ এক-thread-এর কোড কিছুটা ধীর। 3.13-এ এই খরচ বেশ চোখে পড়ার মতো ছিল; 3.14-এ তা অনেক কমেছে (Python-এর ডকুমেন্টেশন অনুযায়ী মোটামুটি ৫–১০%), তবে কাজভেদে আলাদা।
- Race condition আরও সহজে দেখা দেয়। Thread সত্যিই একসাথে চললে
balance += 100-এর lost update নিয়মিত ঘটবে। অর্থাৎ GIL উঠে গেলে Lock আর Queue-র গুরুত্ব আরও বাড়ে, কমে না। Built-in list বা dict-এর ভেতরের গঠন অবশ্য এই build-এও ভাঙে না — CPython সেগুলো নিজেই সুরক্ষিত রাখে; আপনার নিজের যুক্তির race আপনাকেই সামলাতে হবে।
আরও একটি নতুন পথ আছে, শুধু নাম জেনে রাখুন: Python 3.14-এ এসেছে concurrent.interpreters module আর InterpreterPoolExecutor — একই process-এর ভেতরে একাধিক আলাদা interpreter, প্রত্যেকের নিজস্ব GIL. এটি thread আর process-এর মাঝামাঝি একটি পথ, এখনো নতুন।
৯ · কোনটা কখন — বেছে নেওয়ার নিয়ম আর আগে মাপা
এত হাতিয়ার দেখে বিভ্রান্ত হওয়া স্বাভাবিক। নিচের টেবিলটি বেশিরভাগ সিদ্ধান্তের জন্য যথেষ্ট:
| কাজের ধরন | প্রথম পছন্দ | কেন |
|---|---|---|
| I/O-bound, কয়েক থেকে কয়েকশো কাজ (API ডাকা, ফাইল নামানো) | ThreadPoolExecutor | সহজ, সাধারণ library-র সাথেই চলে; অপেক্ষার সময় GIL ছাড়া থাকে |
| I/O-bound, হাজার হাজার একসাথে সংযোগ (chat সার্ভার, বিশাল scraper) | asyncio (পর্ব ৪২) | এক thread-এই হাজারো অপেক্ষা সামলায়, thread-এর খরচ ছাড়া |
| CPU-bound, বিশুদ্ধ Python হিসাব | ProcessPoolExecutor | প্রতিটি process-এর নিজস্ব GIL, সব core কাজে লাগে |
| CPU-bound, সংখ্যার বড় array বা টেবিল | NumPy / pandas-এর vectorized হিসাব (পর্ব ৪৬) | লুপ চলে C-তে, প্রায়ই সমান্তরাল করার চেয়েও বড় লাভ |
| CPU-bound, ভারী ডেটা ভাগাভাগি, library-গুলো প্রস্তুত | free-threaded build + thread | pickle-এর খরচ ছাড়া সত্যিকারের parallel thread |
| ছোট স্ক্রিপ্ট, কয়েক সেকেন্ডে শেষ | কিছুই না — সাধারণ কোড | জটিলতা আর bug-এর ঝুঁকি লাভের চেয়ে বেশি |
সমান্তরাল করার আগে মাপুন
অভিজ্ঞ প্রোগ্রামারের প্রথম প্রশ্ন “কীভাবে সমান্তরাল করব?” নয়, বরং “সময় আসলে কোথায় যাচ্ছে?” হয়তো দেখবেন ৯০% সময় যাচ্ছে একটি ভুল নকশার লুপে, যা একটি dict ব্যবহার করলেই ১০০ গুণ দ্রুত হয় — কোনো thread ছাড়াই। কীভাবে cProfile আর timeit দিয়ে মাপতে হয়, তা পর্ব ৪৩-এ (পারফরম্যান্স — মেপে দ্রুত করা) শিখবেন।
আর মনে রাখুন Amdahl's law: প্রোগ্রামের যে অংশ সমান্তরাল করা যায় না, সেটিই শেষ পর্যন্ত গতির সীমা ঠিক করে। ধরুন প্রোগ্রামের ৮০% ভাগ করা যায়, ২০% যায় না। অসীম core দিলেও ওই ২০% সময় থাকবেই — তাই সর্বোচ্চ লাভ ৫ গুণ। ৪টি core-এ লাভ মাত্র ২.৫ গুণ। Process চালু করা আর ডেটা পাঠানোর খরচ যোগ করলে আরও কম।
- Concurrency আর parallelism-এর পার্থক্য কী? — Concurrency হলো অনেক কাজ একই সময়-পর্বে সামলানো (পালা করে হলেও); parallelism হলো একই মুহূর্তে একাধিক core-এ একাধিক কাজ চলা।
- GIL কী, আর এটি কেন আছে? — সাধারণ CPython-এর একটি তালা, যা একসাথে একটি thread-কেই bytecode চালাতে দেয়; reference counting আর C extension-কে সরলভাবে নিরাপদ রাখতে।
- Thread বনাম process — কখন কোনটি? — Thread একই মেমরি ভাগ করে, হালকা, I/O-bound কাজে ভালো; process-এর আলাদা মেমরি ও GIL, CPU-bound কাজে ভালো।
- GIL থাকলে Lock লাগে কেন? — GIL একটি bytecode নির্দেশ সুরক্ষিত রাখে,
x += 1-এর মতো কয়েক নির্দেশের কাজ নয়; নির্দেশের মাঝে পালা বদল হতে পারে। - Daemon thread কী? — যে thread-এর জন্য প্রোগ্রাম শেষে অপেক্ষা করা হয় না; main thread শেষ হলে হঠাৎ থেমে যায়।
১০ · AI-এর যুগে — concurrency-কোডে AI-এর সাধারণ ভুল
“আমার স্ক্রিপ্টটা দ্রুত করে দাও” বললে AI প্রায়ই সঙ্গে সঙ্গে thread বা process যোগ করে দেয়। কোড দেখতে পেশাদার লাগে, চলেও। কিন্তু concurrency-র bug সবচেয়ে লুকানো bug — বেশিরভাগ সময় ঠিক চলে, মাঝে মাঝে ভুল করে। আর এ বিষয়ে AI-এর প্রশিক্ষণ-ডেটার বড় অংশ free-threading আসার আগের। সাধারণ ভুলগুলো চিনে রাখুন:
| AI প্রায়ই যা করে বা বলে | সমস্যা | কী করবেন |
|---|---|---|
CPU-ভারী বিশুদ্ধ Python লুপে ThreadPoolExecutor | সাধারণ CPython-এ GIL-এর কারণে প্রায় কোনো লাভ নেই | Process, NumPy বা আগে অ্যালগরিদম ঠিক করা |
| ভাগ করা counter বা list-এ তালা ছাড়া লেখা | Race condition — আজ ঠিক, কাল ভুল | with lock: বা প্রতিটি কাজের আলাদা ফল ফেরত দেওয়া |
start() করে join() ভুলে যাওয়া | কাজ শেষের আগেই ফল পড়া | Executor-এর with ব্লক বা স্পষ্ট join |
multiprocessing কোডে main guard নেই | Windows/macOS-এ RuntimeError | সব pool-কোড if __name__ == "__main__":-এর ভেতরে |
| “Python-এ parallelism একেবারেই সম্ভব নয়” | পুরোনো ও ভুল — process সবসময় ছিল, 3.14-এ free-threaded build সমর্থিত | Version উল্লেখ করে প্রশ্ন করুন |
| “3.13 থেকে Python-এ GIL নেই” | উল্টো দিকের ভুল — ডিফল্ট build-এ GIL এখনো আছে | sys._is_gil_enabled() দিয়ে যাচাই |
AI-কে গতি বাড়াতে বলার আগে কাজের ধরন আর পরিবেশ বলে দিন। নিচের কাঠামোটি কপি করে ব্যবহার করুন:
AI-এর concurrency-কোড পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১GIL ১৯৯২ সালে তৈরি হয়েছিল। তখন এটি বুদ্ধিমান সিদ্ধান্ত ছিল কেন, আর multi-core যুগেও প্রায় তিন দশক এটি টিকে রইল কেন?
১৯৯২ সালে Guido van Rossum যখন Python-এ thread যোগ করেন, তখন সাধারণ কম্পিউটারে CPU ছিল একটিই। Thread-এর মূল উদ্দেশ্য ছিল অপেক্ষা সামলানো — একটি thread ফাইল বা নেটওয়ার্কের জন্য আটকে থাকলে অন্যটি কাজ চালিয়ে যাক। এক core-এ “একসাথে দুটি thread bytecode চালানো” এমনিতেই অসম্ভব ছিল, তাই একটি বড় তালার কোনো বাস্তব খরচ ছিল না।
অথচ লাভ ছিল বিশাল:
- সরল ও দ্রুত interpreter: reference count, dict, list — interpreter-এর ভেতরের সব কাঠামো একটি তালায় সুরক্ষিত; প্রতিটি object-এ আলাদা তালা লাগানোর খরচ নেই, তাই এক-thread-এর কোড দ্রুত।
- C extension লেখা সহজ: extension লেখকরা ধরে নিতে পারতেন, তাঁদের C কোড চলার সময় অন্য কেউ Python object বদলাচ্ছে না। এই সরলতা থেকেই Python দ্রুত C library মোড়ানোর সবচেয়ে জনপ্রিয় “আঠা”-ভাষা হয়ে ওঠে।
টিকে থাকার কারণ: ২০০৫-এর পর multi-core CPU সাধারণ হলো, কিন্তু ততদিনে হাজার হাজার C extension GIL-এর নিশ্চয়তার ওপর দাঁড়িয়ে। GIL সরানো মানে সেই অলিখিত চুক্তি ভাঙা। Guido-র অবস্থান ছিল স্পষ্ট: GIL সরানোর যেকোনো প্রস্তাব গ্রহণযোগ্য হবে শুধু তখনই, যদি তা এক-thread-এর প্রোগ্রামকে ধীর না করে আর extension-গুলো না ভাঙে। আগের চেষ্টাগুলো ঠিক এই শর্তে ব্যর্থ হয়। তাছাড়া বাস্তবে অনেক প্রয়োজন অন্য পথে মিটছিল — I/O-তে thread চলত, ভারী হিসাব হতো NumPy-র C কোডে (যা GIL ছেড়ে দেয়), বাকিটা multiprocessing (Python 2.6, ২০০৮ থেকে standard library-তে)। ফলে “GIL সরাও” দাবি জোরালো হলেও এমন জরুরি হয়নি যে ecosystem ভাঙার ঝুঁকি নেওয়া যায়।
মূল উপলব্ধি: একটি নকশা-সিদ্ধান্ত তার সময়ের হার্ডওয়্যার আর ব্যবহারের প্রেক্ষাপটে বিচার করতে হয়। কোনো সিদ্ধান্ত যত বেশি মানুষের কোডের ভিত্তি হয়, তা বদলানোর খরচ তত বাড়ে — কখনো মূল সমস্যার চেয়েও বেশি।
প্র ০২২০০৯–২০১০ সালে David Beazley-র GIL নিয়ে বক্তৃতাগুলো কী দেখিয়েছিল, আর তার পর Python-এ কী বদলাল?
David Beazley একজন পরিচিত Python শিক্ষক ও লেখক। ২০০৯ সালে শিকাগোর Python user group-এ “Inside the Python GIL” আর ২০১০ সালে PyCon-এ “Understanding the Python GIL” নামের বক্তৃতায় তিনি GIL-এর ভেতরের আচরণ মেপে দেখান। ফলাফল অনেককে চমকে দিয়েছিল।
- Multi-core-এ আরও ধীর: দুটি CPU-ভারী thread একটি multi-core মেশিনে চালালে মোট সময় একটার পর একটা চালানোর চেয়েও উল্লেখযোগ্যভাবে বেশি লাগছিল। কারণ পুরোনো GIL একটি নির্দিষ্ট সংখ্যক bytecode নির্দেশ (“tick”, ডিফল্ট ১০০) পর ছেড়ে দেওয়া হতো, আর বিভিন্ন core-এ বসা thread-রা তালা পাওয়ার জন্য অপ্রয়োজনীয়ভাবে লড়াই করত, operating system-কে বারবার জাগাত।
- I/O thread-এর দুর্দশা: একটি CPU-ভারী thread পাশে থাকলে I/O-র thread সময়মতো GIL পেত না, ফলে নেটওয়ার্কের সাড়া ধীর হয়ে যেত।
এরপর কী বদলাল: Antoine Pitrou একটি “নতুন GIL” লেখেন, যা Python 3.2 (২০১১)-তে আসে। এতে tick গোনার বদলে সময়ভিত্তিক নিয়ম আসে — অন্য thread অপেক্ষায় থাকলে প্রায় ৫ মিলিসেকেন্ড পর তালা ছাড়তে হবে। এটাই আজকের sys.setswitchinterval(); পুরোনো sys.setcheckinterval() পরে বাদ দেওয়া হয়। নতুন নকশায় অর্থহীন লড়াই অনেক কমে। তবে Beazley ২০১০ সালেই দেখান যে নতুন GIL-এও একটি সমস্যা থাকে — “convoy effect”: I/O thread প্রতিবার অপেক্ষা থেকে ফিরে CPU thread-এর কাছ থেকে তালা ফেরত পেতে পুরো interval অপেক্ষা করে, তাই অনেক ছোট ছোট I/O থাকলে তা ধীর হয়ে যায়।
এই বক্তৃতাগুলোর প্রভাব ছিল প্রযুক্তির চেয়েও বেশি: GIL নিয়ে অস্পষ্ট অভিযোগের জায়গায় এল মাপা তথ্য, যা পরবর্তী সব আলোচনার ভিত্তি হয়।
মূল উপলব্ধি: concurrency-র আচরণ অনুমান দিয়ে নয়, মেপে বুঝতে হয়। একটি তালা “ঠিক” কাজ করলেও কখন, কাকে, কীভাবে হস্তান্তর হয় — সেই scheduling-এর খুঁটিনাটিই বাস্তব গতি ঠিক করে।
প্র ০৩Larry Hastings-এর “Gilectomy” ব্যর্থ হলো, অথচ Sam Gross-এর “nogil” থেকে PEP 703 গৃহীত হলো। পার্থক্যটা কোথায়?
GIL সরানোর চেষ্টা নতুন নয়। ১৯৯০-এর দশকের শেষে Greg Stein একটি patch বানিয়েছিলেন, যা ছোট ছোট তালা ব্যবহার করত — কিন্তু এক-thread-এর কোড অনেক ধীর হয়ে যায়। ২০১৬ সালের PyCon-এ core developer Larry Hastings ঘোষণা করেন “Gilectomy” (GIL + surgical removal-এর খেলা)। তিনি GIL সরিয়ে reference count-কে atomic (CPU-র বিশেষ অবিভাজ্য নির্দেশ দিয়ে বাড়ানো-কমানো) করেন। কিন্তু atomic নির্দেশ সাধারণ নির্দেশের চেয়ে অনেক দামি, আর একাধিক core একই মেমরি-লাইনে বারবার লিখলে cache-এর মধ্যে টানাটানি হয়। ফলে কয়েকটি core দিয়েও কোড এক-thread-এর GIL-ওয়ালা Python-এর চেয়ে ধীর থাকে। কয়েক বছর চেষ্টার পর প্রকল্পটি থেমে যায়।
২০২১ সালে Meta-র প্রকৌশলী Sam Gross “nogil” নামে CPython-এর একটি পূর্ণাঙ্গ রূপ দেখান। তাঁর সাফল্য কোনো একক কৌশলে নয়, কয়েকটি কৌশলের সমন্বয়ে:
- Biased reference counting: বাস্তবে বেশিরভাগ object শুধু যে thread তৈরি করেছে, সে-ই ব্যবহার করে। তাই মালিক thread সাধারণ (দ্রুত) নির্দেশে গণনা বদলায়; শুধু অন্য thread-রা ধীর atomic পথ নেয়।
- Immortal object:
None,True, ছোট int-এর মতো সবার ব্যবহৃত object-এর গণনা আর বদলানোই হয় না — সবচেয়ে বেশি টানাটানির জায়গাটাই বাদ। - Thread-নিরাপদ মেমরি allocator (mimalloc) আর list/dict-এর জন্য হালকা, প্রতি-object তালা।
এতে এক-thread-এর খরচ গ্রহণযোগ্য সীমায় আসে, আর বহু core-এ সত্যিকারের গতি দেখা যায়। ২০২৩ সালে Steering Council PEP 703 গ্রহণ করে — তবে তিন ধাপে: প্রথমে পরীক্ষামূলক আলাদা build (3.13), তারপর সমর্থিত কিন্তু ঐচ্ছিক (3.14, PEP 779), আর শেষে হয়তো ডিফল্ট — যদি ecosystem প্রস্তুত হয় আর গতির হিসাব মেলে। প্রয়োজন হলে পিছিয়ে আসার পথও খোলা রাখা হয়েছে।
মূল উপলব্ধি: কঠিন প্রকৌশল-সমস্যায় জয় আসে বাস্তব ব্যবহারের ধরন মেপে, সেই অনুযায়ী সাধারণ ঘটনাকে দ্রুত আর বিরল ঘটনাকে নিরাপদ করে। আর বড় ecosystem-এ পরিবর্তন আনতে প্রযুক্তির মতোই জরুরি ধাপে ধাপে, ফেরার পথ রেখে এগোনো।
প্র ০৪Amdahl's law কী বলে? ৮ core-এর মেশিনে prime গোনার কাজ ৮ গুণ দ্রুত হয় না কেন?
১৯৬৭ সালে কম্পিউটার স্থপতি Gene Amdahl একটি সম্মেলনে যুক্তি দেন: একটি প্রোগ্রামের যে অংশ সমান্তরাল করা যায় না, সেটিই শেষ পর্যন্ত মোট গতির সীমা ঠিক করে। সূত্রটি সহজ — যদি প্রোগ্রামের p অংশ ভাগ করা যায় আর nটি core থাকে, তাহলে সর্বোচ্চ লাভ = 1 / ((1 − p) + p / n).
p = 0.9,n = 8: লাভ = 1 / (0.1 + 0.1125) ≈ ৪.৭ গুণ — ৮ নয়।p = 0.9,nঅসীম: লাভ সর্বোচ্চ ১০ গুণ। ১০০০ core দিলেও ওই ১০% সময় থেকেই যায়।
Prime গোনার ক্ষেত্রে বাস্তবে আরও কিছু যোগ হয়:
- ক্রমিক অংশ: process চালু করা, interpreter লোড, পরিসর ভাগ করা, ফল যোগ করা — এগুলো সমান্তরাল নয়।
- অসম ভাগ: বড় সংখ্যার prime পরীক্ষায় বেশি ভাজক দেখতে হয়, তাই
75000–100000-এর ভাগটি0–25000-এর চেয়ে বেশি সময় নেয়। সবাই শেষ না হওয়া পর্যন্তmapঅপেক্ষা করে, তাই সবচেয়ে ধীর কর্মীই মোট সময় ঠিক করে। ছোট ছোট অনেক খণ্ডে ভাগ করলে (বাchunksize) ভার আরও সমান হয়। - হার্ডওয়্যার: অনেক CPU-তে “৮ core” মানে সব সমান নয় (performance আর efficiency core), আর একসাথে সব core চললে ঘড়ির গতি কমে যেতে পারে।
এর একটি বিপরীত দৃষ্টিভঙ্গিও আছে — ১৯৮৮ সালে John Gustafson দেখান, বাস্তবে core বাড়লে মানুষ সাধারণত সমস্যার আকারও বাড়ায়, তখন সমান্তরাল অংশের ভাগ বাড়ে আর লাভ আরও ভালো দেখায়। দুটো দৃষ্টিভঙ্গিই সত্য, ভিন্ন প্রশ্নের উত্তর।
মূল উপলব্ধি: core যোগ করা গতির জাদুকরি সুইচ নয়। আগে মাপুন কোন অংশ ভাগ করা যায়, ক্রমিক অংশ কমান, ভার সমান করুন — তবেই বাড়তি core-এর মূল্য পাবেন।
প্র ০৫Rob Pike-এর বিখ্যাত বক্তৃতার শিরোনাম “Concurrency is not parallelism”. এ কথার অর্থ কী, আর Python প্রোগ্রামারের জন্য এর শিক্ষা কী?
Rob Pike — Unix-এর প্রথম দিকের গবেষক, UTF-8-এর সহ-নকশাকার, আর Go ভাষার অন্যতম স্রষ্টা — ২০১২ সালের জানুয়ারিতে Heroku-র Waza সম্মেলনে এই শিরোনামে একটি বক্তৃতা দেন, যা পরে এই বিষয়ের সবচেয়ে উদ্ধৃত ব্যাখ্যাগুলোর একটি হয়ে যায়। তাঁর মূল কথা: concurrency হলো অনেক কাজ একসাথে সামলানোর জন্য প্রোগ্রামের কাঠামো; parallelism হলো অনেক কাজ একসাথে করা — একটি চলার সময়ের ঘটনা। ভালো concurrent কাঠামো parallelism-কে সম্ভব করে, কিন্তু নিশ্চিত করে না।
তাঁর উদাহরণ ছিল বইয়ের স্তূপ চুল্লিতে পোড়ানো গোফারদের দল — কাজটিকে স্বাধীন ধাপে (আনা, ঠেলাগাড়িতে তোলা, পোড়ানো) ভাগ করলে, এক গোফার থাকলেও নকশাটি পরিষ্কার; বেশি গোফার এলে সেই কাঠামোই সহজে সমান্তরাল হয়।
Python-এর জন্য শিক্ষা:
- সাধারণ CPython-এ thread দিয়ে লেখা প্রোগ্রাম concurrent, কিন্তু বিশুদ্ধ Python অংশে parallel নয়। তবু I/O-তে লাভ হয়, কারণ অপেক্ষাগুলো একসাথে চলে।
asyncio(পর্ব ৪২) একটিমাত্র thread-এ চলে — এটি parallelism একেবারেই দেয় না, অথচ হাজারো সংযোগ সামলানোর জন্য দুর্দান্ত concurrency দেয়।- উপরের রেস্টুরেন্টের Queue নকশাটি একটি concurrent কাঠামো। Free-threaded build-এ বা বাবুর্চিদের process বানালে একই কাঠামো parallel হয়ে যায়, প্রায় কোনো নকশা না বদলেই।
তাই প্রথমে কাজটিকে স্বাধীন অংশে ভাগ করার কথা ভাবুন, যারা বার্তা দিয়ে কথা বলে আর কম ডেটা ভাগ করে। কোন runtime-এ চালাবেন — thread, process, asyncio, free-threaded — সেটা পরের সিদ্ধান্ত।
মূল উপলব্ধি: concurrency হলো নকশার গুণ, parallelism হলো হার্ডওয়্যারের সুযোগ। ভালো নকশা থাকলে সুযোগ এলে তা কাজে লাগানো সহজ; খারাপ নকশায় বেশি core শুধু বেশি bug আনে।
অনুশীলন
এগুলোর বেশিরভাগে নতুন কোড লিখতে হবে না — শ্রেণিবিভাগ, অনুমান আর হিসাবই আসল কাজ।
-
কাজ চিনুন: নিচের প্রতিটি কাজ I/O-bound না CPU-bound, আর সাধারণ CPython-এ কোন পদ্ধতি নেবেন? (ক) ৫০০টি পণ্যের ছবি একটি সার্ভার থেকে নামানো; (খ) ৫০০টি ছবির আকার ছোট করা, বিশুদ্ধ Python-এ pixel ধরে ধরে; (গ) একটি ১০ GB log ফাইল থেকে error লাইন খোঁজা; (ঘ) ২০টি ডেটাবেস query একসাথে পাঠানো।
(ক) I/O-bound —
ThreadPoolExecutor(কয়েকজন কর্মী, সার্ভারের ওপর চাপ বিবেচনা করে)। (খ) CPU-bound —ProcessPoolExecutor; আরও ভালো হয় Pillow বা NumPy-র মতো library ব্যবহার করলে, যাদের ভেতরের কাজ C-তে চলে। (গ) মিশ্র — বড় অংশ ডিস্ক পড়া (I/O), আর প্রতিটি লাইন মেলানো CPU-র কাজ; সাধারণত একটি সরল লুপই যথেষ্ট দ্রুত, আগে মেপে দেখুন। (ঘ) I/O-bound — thread pool বা asyncio-সমর্থিত ডেটাবেস driver. -
তালা সরিয়ে ভাবুন:
bank_lock.py-তেwith lock:লাইনটি মুছেbalance += 100-কে এক ঘর বাঁয়ে সরালে কী হতে পারে? চালিয়ে যদি ঠিক 4000000-ই আসে, তাহলে কি কোডটি নিরাপদ প্রমাণ হলো?তালা না থাকলে read-modify-write-এর মাঝখানে পালা বদল হলে জমা হারাতে পারে, ফল 4000000-এর কম হতে পারে, আর প্রতিবার আলাদা। সাম্প্রতিক সাধারণ CPython-এ পালা বদল কিছু নির্দিষ্ট জায়গায় হয় বলে আপনি হয়তো বারবার ঠিক ফলই দেখবেন। কিন্তু এটা প্রমাণ নয় — ভাষা কোনো নিশ্চয়তা দেয় না, আর অন্য version বা free-threaded build-এ (
python3.14t) একই কোড নিয়মিত ভুল দেবে। Race condition-এর ক্ষেত্রে “চালিয়ে ঠিক এল” মানে শুধু “এবার ভাগ্য ভালো ছিল”। -
Amdahl-এর হিসাব: একটি রিপোর্ট তৈরির প্রোগ্রাম ১০০ সেকেন্ড চলে। এর মধ্যে ২০ সেকেন্ড ডেটাবেস থেকে ডেটা পড়া (ভাগ করা যায় না), ৮০ সেকেন্ড হিসাব (পুরোপুরি ভাগ করা যায়)। ৪টি process-এ মোট সময় কত হবে, আর অসীম core দিলে সর্বনিম্ন কত?
৪টি process-এ: ২০ + ৮০/৪ = ৪০ সেকেন্ড, অর্থাৎ ২.৫ গুণ দ্রুত (process চালু আর ডেটা পাঠানোর খরচ ধরলে একটু বেশি)। অসীম core-এ: হিসাবের অংশ প্রায় শূন্যে নামলেও ২০ সেকেন্ড থাকবেই — সর্বোচ্চ ৫ গুণ লাভ। তাই এখানে পরের বড় উন্নতি আসবে ডেটা পড়ার অংশ দ্রুত করে (ভালো query, index, অথবা পড়া আর হিসাব একসাথে চালানো), আরও core কিনে নয়।
-
AI অডিট: যেকোনো AI-কে বলুন: “Python-এ ১ থেকে ১ কোটি পর্যন্ত prime সংখ্যা গোনার কোড দ্রুত করো, multithreading ব্যবহার করে।” উত্তরটি এই পর্বের চেকলিস্ট দিয়ে যাচাই করুন। AI কি thread দিয়ে CPU-bound কাজের সমস্যা উল্লেখ করল?
প্রায়ই AI সরাসরি
ThreadPoolExecutorবাthreadingদিয়ে কোড দেয়, কারণ আপনি thread চেয়েছেন। ভালো উত্তর হলে সে সতর্ক করবে যে সাধারণ CPython-এ GIL-এর কারণে এতে গতি বাড়বে না, আরProcessPoolExecutor(main guard সহ), অ্যালগরিদম উন্নতি (যেমন Sieve of Eratosthenes, যা এখানে যেকোনো সমান্তরাল কৌশলের চেয়ে বড় লাভ দেয়), NumPy, অথবা free-threaded build-এর কথা বলবে। খেয়াল করুন সে main guard দিয়েছে কি না, আর ফল ছাপার আগে সব কাজ শেষ হওয়ার নিশ্চয়তা আছে কি না। শিক্ষা: প্রশ্নে পদ্ধতি চাপিয়ে দিলে AI প্রায়ই সেই পদ্ধতিই দেয়, ভালোটা নয় — তাই সমস্যা বলুন, সমাধান নয়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমProcessPoolExecutor.map দিয়ে একটি কাজ চালানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🛡️ মূল process-এ
if __name__ == "__main__":-এর ভেতরের কোড চলে - 🏭 Pool তৈরি হয় — worker process-গুলো চালু হয়
- 📦 প্রতিটি argument pickle হয়ে worker-এর কাছে যায়
- ⚙️ Worker process-এ ফাংশনটি নিজের GIL নিয়ে চলে
- 📬 ফল pickle হয়ে মূল process-এ ফেরে, ইনপুটের ক্রমে সাজানো হয়
- 🚪
withব্লক শেষে pool সব worker বন্ধ করে
সাধারণ (GIL সহ) CPython-এ ২০টি ওয়েবপাতা নামানোর কাজে thread ব্যবহার করলে কী হয়?
- GIL-এর কারণে কোনো লাভ হয় না, সময় একই থাকে
- মোট সময় অনেক কমে, কারণ অপেক্ষার সময় GIL ছেড়ে দেওয়া হয়
- প্রোগ্রাম crash করে, কারণ GIL একাধিক thread অনুমতি দেয় না
- শুধু process দিয়েই এই কাজ দ্রুত করা সম্ভব
একাধিক thread থেকে counter += 1 চালানো তালা ছাড়া অনিরাপদ কেন?
- int overflow হয়ে যেতে পারে
- GIL চিরকালের জন্য একটি thread-এ আটকে যায়
- এটি read-modify-write — ধাপগুলোর মাঝে অন্য thread ঢুকে আপডেট হারাতে পারে
- thread-এর ভেতরে += অপারেটর ব্যবহার করা যায় না
ThreadPoolExecutor-এর map(fn, items) ফল কোন ক্রমে দেয়?
- items-এর ক্রমে, কাজ যে ক্রমেই শেষ হোক
- যে কাজ আগে শেষ হয়, তার ফল আগে
- ফলের মান অনুযায়ী ছোট থেকে বড়
- ক্রম এলোমেলো, প্রতিবার আলাদা
Windows-এ ProcessPoolExecutor ব্যবহারের কোডে if __name__ == "__main__": লাগে কেন?
- এতে process-গুলো দ্রুত চলে
- guard ছাড়া argument pickle করা যায় না
- Windows-এ GIL বন্ধ করতে এটি লাগে
- নতুন process মূল ফাইল import করে, guard না থাকলে আবার pool তৈরির কোড চলত
Python 3.14-এ free-threaded (GIL-ছাড়া) build-এর অবস্থা কোনটি?
- এটিই এখন ডিফল্ট, সব Python-এ GIL সরানো হয়েছে
- অফিসিয়ালি সমর্থিত, কিন্তু আলাদা ও ঐচ্ছিক build
- শুধু পরীক্ষামূলক, কোনো সমর্থন নেই
- প্রস্তাবটি বাতিল হয়েছে, এমন কোনো build নেই
সাধারণ CPython-এ ৮ core-এর মেশিনে বিশুদ্ধ Python-এ লেখা একটি ভারী prime-গোনার কাজ দ্রুত করতে কোনটি সবচেয়ে কার্যকর?
- ৮টি thread সহ ThreadPoolExecutor
- asyncio দিয়ে কাজগুলো async ফাংশনে লেখা
- কাজ ভাগ করে ProcessPoolExecutor ব্যবহার
- thread-গুলোকে daemon=True করা
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Concurrency = অনেক কাজ সামলানো; parallelism = একই মুহূর্তে একাধিক core-এ চলা। আগে চিনুন কাজটি I/O-bound না CPU-bound.
- সাধারণ CPython-এ GIL একসাথে একটি thread-কেই bytecode চালাতে দেয়; অপেক্ষার সময় ছেড়ে দেয় — তাই thread I/O-তে কাজের, বিশুদ্ধ Python হিসাবে নয়।
ThreadPoolExecutor/ProcessPoolExecutor-এরmapফল দেয় ইনপুটের ক্রমে;as_completedদেয় শেষ হওয়ার ক্রমে।x += 1অবিভাজ্য নয় — ভাগ করা ডেটায়with lock:, অথবাqueue.Queueদিয়ে বার্তা আদান-প্রদান; একাধিক তালা সবসময় একই ক্রমে।- CPU-ভারী কাজে process (main guard, pickle, চালুর খরচ মনে রেখে), NumPy, অথবা 3.14-এর ঐচ্ছিক free-threaded build — আর সবকিছুর আগে মাপুন.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪২ · asyncio — async/awaitপরবর্তী পর্বএক thread-এ হাজারো অপেক্ষা — I/O-bound কাজের আরেক পথ।
- Python প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — ৮টি মডিউল, কোন পর্ব কখন।
- C++ multithreading — thread, mutex আর condition variableতুলনা করুনGIL ছাড়া একটি ভাষায় thread, data race আর তালার পুরো ছবি।
- Operating Systemsগভীরে যানProcess, thread, scheduling আর deadlock — operating system-এর দৃষ্টিতে।