Thread Pool — একটি থ্রেড ব্যবস্থাপনা প্রক্রিয়া যেখানে পূর্বনির্মিত থ্রেডের একটি পুল কাজ সম্পাদনের জন্য পুনরায় ব্যবহার করা হয়, থ্রেড তৈরি এবং ধ্বংস করার ওভারহেড এড়িয়ে। মোবাইল ডেভেলপমেন্টে, থ্রেড পুল ব্যাকগ্রাউন্ড অপারেশনের জন্য ব্যবহৃত হয়: নেটওয়ার্ক অনুরোধ, ইমেজ প্রসেসিং, ডেটাবেসের সাথে কাজ। Google Android ডকুমেন্টেশন (2025) অনুসারে, ExecutorService Android-এ ব্যাকগ্রাউন্ড থ্রেড পরিচালনার জন্য প্রস্তাবিত পদ্ধতি। iOS-এ, OperationQueue এবং GCD DispatchQueue গ্লোবাল concurrent কিউ সহ অনুরূপ ভূমিকা পালন করে।
মূল পয়েন্ট
Thread Pool (থ্রেড পুল) — একটি আর্কিটেকচারাল প্যাটার্ন যেখানে থ্রেডের একটি নির্দিষ্ট সংখ্যা আগে থেকে তৈরি করা হয় এবং একাধিক কাজ সম্পাদনের জন্য পুনরায় ব্যবহার করা হয়। প্রতিটি অপারেশনের জন্য নতুন থ্রেড তৈরি করার পরিবর্তে (যা ব্যয়বহুল: JVM-এ প্রতি থ্রেডে প্রায় 1 MB স্ট্যাক), কাজগুলি একটি কিউতে রাখা হয় এবং পুলের মুক্ত থ্রেড দ্বারা সম্পাদিত হয়। মোবাইল ডেভেলপমেন্টে, থ্রেড পুল কর্মক্ষমতার জন্য গুরুত্বপূর্ণ — Android এবং iOS প্রতি অ্যাপ্লিকেশনে থ্রেডের সংখ্যা সীমিত করে।
থ্রেড তৈরি করা একটি ব্যয়বহুল অপারেশন: স্ট্যাক বরাদ্দ, সিস্টেম নিবন্ধন, প্রসঙ্গ সুইচিং। সীমিত সম্পদযুক্ত মোবাইল ডিভাইসে, থ্রেডের অনিয়ন্ত্রিত তৈরি Android-এ OOM (OutOfMemoryError) এবং iOS-এ থ্রটলিংয়ের দিকে নিয়ে যায়। Thread Pool উভয় সমস্যার সমাধান করে: এটি একসাথে চলমান থ্রেডের সর্বোচ্চ সংখ্যা সীমিত করে এবং ইতিমধ্যে তৈরি থ্রেড পুনরায় ব্যবহার করে। Google raw Thread()-এর পরিবর্তে ExecutorService সুপারিশ করে, Apple Thread-এর পরিবর্তে OperationQueue সুপারিশ করে।
| প্যারামিটার | পুল ছাড়া (raw Thread) | Thread Pool সহ |
|---|---|---|
| থ্রেড তৈরি | প্রতি কাজের জন্য | পুল তৈরি করার সময় একবার |
| সর্বোচ্চ থ্রেড | অসীমিত (OOM ঝুঁকি) | core/max pool size দ্বারা সীমিত |
| ব্যবহার | কম (কাজের পর থ্রেড মারা যায়) | উচ্চ (থ্রেড পুনরায় ব্যবহৃত হয়) |
| ব্যবস্থাপনা | ম্যানুয়াল (join, interrupt) | স্বয়ংক্রিয় (ExecutorService) |
| মেমোরি খরচ | প্রতি কাজের সাথে বাড়ে | স্থির |
থ্রেড পুল উৎপাদক-ভোক্তা নীতিতে কাজ করে: কাজগুলি (Runnable/Callable) একটি ব্লকিং কিউতে (BlockingQueue) রাখা হয়। পুলের থ্রেডগুলি কিউতে কাজের অপেক্ষা করে এবং সেগুলি সম্পাদনের জন্য নিয়ে নেয়। অ্যালগরিদম: যদি মুক্ত থ্রেডের সংখ্যা corePoolSize-এর চেয়ে কম হয়, একটি নতুন থ্রেড তৈরি করা হয়। যদি corePoolSize পৌঁছে যায়, কাজটি কিউতে রাখা হয়। যদি কিউ পূর্ণ হয় এবং থ্রেডের সংখ্যা maximumPoolSize-এর চেয়ে কম হয়, একটি অতিরিক্ত থ্রেড তৈরি করা হয়। যদি maximumPoolSize অতিক্রম করা হয়, কাজটি RejectedExecutionHandler-এর মাধ্যমে প্রত্যাখ্যান করা হয়।
Core pool size — থ্রেডের সংখ্যা যা নিষ্ক্রিয় থাকলেও পুলে রাখা হয়। Maximum pool size — থ্রেডের সর্বোচ্চ সংখ্যা যা কিউ ওভারফ্লো হলে তৈরি করা যেতে পারে। তাদের মধ্যে পার্থক্য হল অতিরিক্ত (overflow) থ্রেড যা অস্থায়ীভাবে তৈরি করা হয় এবং নিষ্ক্রিয়তা টাইমআউটের পরে শেষ হয়ে যায়। মোবাইল ডিভাইসে, থ্রেড তৈরি থেকে পিক লোড এড়াতে corePoolSize কে maximumPoolSize-এর সমান সেট করার সুপারিশ করা হয়।
BlockingQueue সম্পাদনের অপেক্ষায় থাকা কাজগুলি সংরক্ষণ করে। সবচেয়ে জনপ্রিয় বাস্তবায়ন: LinkedBlockingQueue (অসীমিত), ArrayBlockingQueue (সীমিত) এবং SynchronousQueue (স্টোরেজ ছাড়া — কাজ সরাসরি থ্রেডে পাঠানো হয়)। যখন কিউ এবং পুল পূর্ণ হয়, RejectedExecutionHandler সক্রিয় হয়। মানক নীতিগুলি: AbortPolicy (RejectedExecutionException নিক্ষেপ করে), CallerRunsPolicy (প্রেরকের থ্রেডে সম্পাদন করে), DiscardPolicy এবং DiscardOldestPolicy।
// Android-এ Thread Pool তৈরি
val threadPool = ThreadPoolExecutor(
corePoolSize = 2, // সর্বনিম্ন 2 থ্রেড
maximumPoolSize = 4, // সর্বোচ্চ 4 থ্রেড
keepAliveTime = 30L, // ওভারফ্লো থ্রেডের জীবনকাল
unit = TimeUnit.SECONDS,
workQueue = LinkedBlockingQueue<Runnable>(16),
threadFactory = Executors.defaultThreadFactory(),
handler = ThreadPoolExecutor.CallerRunsPolicy()
)
// কাজ জমা দেওয়া
threadPool.execute {
val result = api.fetchData()
runOnUiThread { showData(result) }
}
// পুল বন্ধ করা
threadPool.shutdown()
// সব কাজ শেষ হওয়ার অপেক্ষা
threadPool.awaitTermination(10, TimeUnit.SECONDS)
Android java.util.concurrent-এর মাধ্যমে থ্রেড পুলের একাধিক বাস্তবায়ন প্রদান করে। Executors — প্রস্তুত কনফিগারেশন সহ একটি কারখানা: newFixedThreadPool(n) (স্থির পুল), newCachedThreadPool() (অসীমিত, প্রয়োজন অনুযায়ী থ্রেড তৈরি), newSingleThreadExecutor() (একটি থ্রেড — অনুক্রমিক সম্পাদন)। মোবাইল প্রকল্পের জন্য, যুক্তিসঙ্গত সীমা (2-4 থ্রেড) সহ newFixedThreadPool সুপারিশ করা হয়, কারণ ক্যাশড পুল অনেক বেশি থ্রেড তৈরি করতে পারে।
ThreadPoolExecutor (TPE) — কনফিগারযোগ্য প্যারামিটার সহ ExecutorService-এর পূর্ণ বাস্তবায়ন। Android-এ, TPE AsyncTask, IntentService এবং JobIntentService-এর ভিতরে ব্যবহৃত হয়। প্যারামিটার corePoolSize, maximumPoolSize, keepAliveTime, BlockingQueue এবং RejectedExecutionHandler পুল আচরণ সূক্ষ্মভাবে টিউন করার অনুমতি দেয়। Android-এর জন্য সুপারিশ: corePoolSize = CPU কোরের সংখ্যা - 1 (IO-bound কাজের জন্য) বা কোরের সংখ্যা (CPU-bound কাজের জন্য)। সাধারণ অ্যাপ্লিকেশনের জন্য: 2-4 থ্রেড।
// Executors-এর প্রস্তুত কনফিগারেশন
// 1. 3 থ্রেডের স্থির পুল
val fixedPool = Executors.newFixedThreadPool(3)
// 2. ক্যাশড পুল (মোবাইলের জন্য সুপারিশ করা হয় না)
val cachedPool = Executors.newCachedThreadPool()
// 3. একক থ্রেড (সিরিয়ালাইজেশন)
val singlePool = Executors.newSingleThreadExecutor()
// 4. শিডিউলার (পর্যায়ক্রমিক কাজ)
val scheduler = Executors.newScheduledThreadPool(2)
// Callable এবং Future-এর সাথে ব্যবহার
val future: Future<String> = fixedPool.submit(Callable {
"Result: ${api.call()}"
})
// ফলাফল পাওয়া (থ্রেড ব্লক করে)
val result = future.get(5, TimeUnit.SECONDS)
// পুল বন্ধ করা
fixedPool.shutdownNow()
Kotlin করুটিন CoroutineDispatcher প্রদান করে — থ্রেড পুলের মতো একটি অ্যাবস্ট্রাকশন। Dispatchers.IO 64 থ্রেডের (সীমিত) একটি পুল ব্যবহার করে। Dispatchers.Default — CPU কোরের সংখ্যার সমান একটি পুল। CoroutineDispatcher-এর ম্যানুয়াল shutdown-এর প্রয়োজন নেই এবং এটি স্বয়ংক্রিয়ভাবে পরিচালিত হয়। সূক্ষ্ম টিউনিংয়ের জন্য, Executors.newFixedThreadPool(2).asCoroutineDispatcher()-এর মাধ্যমে কাস্টম ExecutorCoroutineDispatcher তৈরি করুন। করুটিন থ্রেড পুল প্রতিস্থাপন করে না, বরং এটি মোড়ক করে।
iOS থ্রেড পুল পরিচালনার জন্য দুটি প্রধান প্রক্রিয়া প্রদান করে: OperationQueue (GCD-এর উপর নির্মিত উচ্চ-স্তরের API) এবং GCD DispatchQueue (নিম্ন-স্তরের C-API)। OperationQueue maxConcurrentOperationCount প্রপার্টির মাধ্যমে থ্রেড পুলকে এনক্যাপসুলেট করে। ডিফল্টরূপে, OperationQueue সিস্টেম-নির্ধারিত সর্বোচ্চ ব্যবহার করে (সিস্টেম লোডের উপর নির্ভর করে)। DispatchQueue.global() সিস্টেম থ্রেড পুল সহ একটি concurrent কিউ প্রদান করে।
OperationQueue maxConcurrentOperationCount-এর মাধ্যমে থ্রেড পুল পরিচালনা করে। মান 1 একটি সিরিয়াল কিউ তৈরি করে (একক থ্রেড পুলের অনুরূপ)। 1-এর বেশি মান নির্দিষ্ট সীমা সহ একটি concurrent পুল তৈরি করে। ডিফল্টরূপে, maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (সিস্টেম অপটিমাম, সাধারণত 4-8 থ্রেড)। Operation নির্ভরতা, অগ্রাধিকার এবং বাতিলকরণ সমর্থন করে। প্রতিটি অপারেশন সিস্টেম পুলের যেকোনো উপলব্ধ থ্রেডে সম্পাদিত হয়।
// 3 থ্রেডের পুল সহ OperationQueue
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility
// অপারেশন তৈরি
let operation1 = BlockOperation {
let data = fetchData(from: url1)
DispatchQueue.main.async { updateUI(data) }
}
let operation2 = BlockOperation {
let data = fetchData(from: url2)
DispatchQueue.main.async { updateUI(data) }
}
// নির্ভরতা: operation2, operation1-এর অপেক্ষা করে
operation2.addDependency(operation1)
// কিউতে যোগ করা
queue.addOperations([operation1, operation2], waitUntilFinished: false)
// সব অপারেশন বাতিল করা
queue.cancelAllOperations()
DispatchQueue — Apple-এর থ্রেড পুল। একটি concurrent কিউ (qos: .utility) সিস্টেম থ্রেড পুল ব্যবহার করে, যা বর্তমান ডিভাইস লোডের জন্য অপটিমাইজড। বিভিন্ন QoS স্তর (userInteractive, userInitiated, utility, background) বিভিন্ন অগ্রাধিকার সহ বিভিন্ন পুলে ম্যাপ হয়। DispatchGroup একাধিক কাজ সিঙ্ক্রোনাইজ করার অনুমতি দেয়। DispatchWorkItem বাতিলকরণ এবং qualityOfService সমর্থন করে। সূক্ষ্ম নিয়ন্ত্রণের জন্য, DispatchQueue(label: qos: attributes: .concurrent)-এর মাধ্যমে কাস্টম concurrent কিউ তৈরি করুন।
// GCD DispatchQueue থ্রেড পুল হিসেবে
let customQueue = DispatchQueue(
label: "com.app.background",
qos: .utility,
attributes: .concurrent,
autoreleaseFrequency: .workItem
)
// পুলে কাজ জমা দেওয়া
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }
// সিঙ্ক্রোনাইজেশনের জন্য DispatchGroup
let group = DispatchGroup()
let pool = DispatchQueue.global(qos: .utility)
pool.async(group: group) { fetchData() }
pool.async(group: group) { processImage() }
group.notify(queue: .main) {
self.showResult() // উভয় কাজ সম্পন্ন
}
// semaphore-এর মাধ্যমে concurrency সীমিত করা
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
pool.async {
semaphore.wait()
download(url)
semaphore.signal()
}
}
থ্রেড পুল কনফিগারেশন সরাসরি অ্যাপ্লিকেশনের কর্মক্ষমতা প্রভাবিত করে। ভুল প্যারামিটার CPU-র কম ব্যবহার (অতি কম থ্রেড) বা সিস্টেম ওভারলোড (অতি বেশি) এর দিকে নিয়ে যায়। মোবাইল অ্যাপ্লিকেশনের জন্য, সীমিত সম্পদ এবং শক্তি খরচের কারণে অপটিমাল মান সার্ভার-সাইড থেকে ভিন্ন হয়। প্রধান প্যারামিটারগুলি হল: corePoolSize, maxPoolSize, কিউ ক্ষমতা এবং keepAliveTime।
IO-bound কাজের জন্য সূত্র: corePoolSize = CPU কোরের সংখ্যা × 2 (থ্রেড I/O-র অপেক্ষা করে)। CPU-bound কাজের জন্য: corePoolSize = CPU কোরের সংখ্যা (থ্রেড ক্রমাগত গণনায় ব্যস্ত থাকে)। আধুনিক মোবাইল ডিভাইসে (6-8 কোর) এটি CPU-bound-এর জন্য 6-8 থ্রেড এবং IO-bound-এর জন্য 12-16 দেয়। ব্যবহারিক পরীক্ষা দেখায় যে সাধারণ মোবাইল অ্যাপ্লিকেশনের জন্য 3-4 থ্রেড অপটিমাল — বেশি থ্রেড কর্মক্ষমতা লাভ ছাড়াই শক্তি খরচ বাড়ায়।
ওয়ার্ক কিউ (work queue) এর আকার নির্ধারণ করে কতগুলি কাজ সম্পাদনের অপেক্ষায় থাকতে পারে। অসীমিত কিউ (সীমা ছাড়া LinkedBlockingQueue) দ্রুত কাজ আগমনে OOM-এর কারণ হতে পারে। সীমিত কিউ (নির্দিষ্ট আকারের ArrayBlockingQueue) পূর্ণ হলে কাজ প্রত্যাখ্যান করে। মোবাইল অ্যাপ্লিকেশনের জন্য, 16-32 কাজের ক্ষমতা সহ ArrayBlockingQueue সুপারিশ করা হয়। CallerRunsPolicy মোবাইলের জন্য সেরা RejectedExecutionHandler: এটি কাজ হারানোর পরিবর্তে প্রেরককে ধীর করে (ব্যাকপ্রেশার)।
| প্যারামিটার | মোবাইলের জন্য সুপারিশ | যুক্তি |
|---|---|---|
| corePoolSize | 2-4 | মোবাইল ডিভাইসের সীমিত সম্পদ |
| maxPoolSize | corePoolSize (বা +1-2) | থ্রেড তৈরি থেকে পিক লোড এড়ানো |
| keepAliveTime | 15-30 সেকেন্ড | বারবার তৈরি ছাড়া দ্রুত মেমোরি মুক্তি |
| কিউ ক্ষমতা | 16-32 | বাফারিং এবং OOM ঝুঁকির মধ্যে ভারসাম্য |
| হ্যান্ডলার | CallerRunsPolicy | কাজ না হারিয়ে ব্যাকপ্রেশার |
মোবাইল অ্যাপ্লিকেশন ডেভেলপাররা প্রায়ই থ্রেড পুল ব্যবহার করার সময় ভুল করে যা ক্র্যাশ, মেমোরি লিক এবং অস্থির অপারেশনের কারণ হয়। সবচেয়ে সাধারণ: ExecutorService-এর জন্য shutdown() না কল করা, প্রতিটি অপারেশনের জন্য নতুন পুল তৈরি করা, খুব বড় পুল, কাজের মধ্যে deadlock, Android-এ CachedThreadPool ব্যবহার করা।
Deadlock ঘটে যখন পুলের একটি কাজ একই পুলের অন্য কাজের ফলাফলের অপেক্ষা করে, কিন্তু সমস্ত থ্রেড অপেক্ষায় ব্যস্ত থাকে। উদাহরণ: কাজ A একই পুলে কাজ B পাঠায় এবং future.get() কল করে — যদি পুল শেষ হয়ে যায়, কাজ A কাজ B-র অপেক্ষা করে, এবং কাজ B সম্পাদন করতে পারে না কারণ কোনও মুক্ত থ্রেড নেই। সমাধান: বিভিন্ন স্তরের কাজের জন্য পৃথক পুল ব্যবহার করুন বা ব্লকিং .get()-এর পরিবর্তে async callback ব্যবহার করুন।
// Thread Pool-এ Deadlock
val pool = Executors.newFixedThreadPool(1)
// কাজ A, কাজ B-র অপেক্ষা করে — deadlock!
val futureA = pool.submit {
// এই কাজ কখনই সম্পাদিত হবে না
val futureB = pool.submit { 42 }
futureB.get() // চিরকালের জন্য ব্লক
}
// সমাধান: পৃথক পুল
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()
workerPool.submit {
callbackPool.submit {
// পৃথক পুলে চলছে — deadlock অসম্ভব
}
}
// বা CompletableFuture ব্যবহার করুন
workerPool.submit {
CompletableFuture
.supplyAsync { 42 }
.thenAccept { result ->
println(result)
}
}
Activity-তে তৈরি ExecutorService onDestroy()-এ বন্ধ করতে হবে। যদি তা না করা হয়, Activity ধ্বংস হওয়ার পরও থ্রেড মেমোরিতে ঝুলে থাকবে। সমাধান: Activity-র পরিবর্তে পুল Application স্কোপ বা ViewModel-এ রাখুন। করুটিনের জন্য, viewModelScope বা lifecycleScope ব্যবহার করুন। যদি পুল Activity-র ভিতরে তৈরি করা হয়, onDestroy()-এ pool.shutdown() কল করা নিশ্চিত করুন। পরীক্ষার জন্য, তাৎক্ষণিক বন্ধের জন্য es.shutdownNow() ব্যবহার করুন।
সচরাচর জিজ্ঞাসিত প্রশ্ন
Thread Pool একাধিক কাজ সম্পাদনের জন্য ইতিমধ্যে তৈরি থ্রেড পুনরায় ব্যবহার করে। একটি সাধারণ থ্রেড (raw Thread) তৈরি হয়, একটি কাজ সম্পাদন করে এবং ধ্বংস হয়। থ্রেড তৈরি করতে প্রায় 1 MB মেমোরি এবং ~1 ms সময় লাগে। Thread Pool ওভারহেড কমায়, থ্রেডের সর্বোচ্চ সংখ্যা সীমিত করে এবং ব্যবস্থাপনার জন্য API (shutdown, awaitTermination) প্রদান করে।
সাধারণ মোবাইল অ্যাপ্লিকেশনের জন্য, 2-4 থ্রেড অপটিমাল। CPU-bound কাজের জন্য — CPU কোরের সংখ্যা। IO-bound কাজের জন্য — কোরের সংখ্যা × 2। বেশি থ্রেড কর্মক্ষমতা লাভ ছাড়াই শক্তি খরচ এবং প্রসঙ্গ সুইচিং বাড়ায়। Android-এ, কোর নির্ধারণ করতে Process.availableProcessors() ব্যবহার করুন। iOS-এ — ProcessInfo.processInfo.processorCount।
CachedThreadPool প্রয়োজন অনুযায়ী থ্রেড তৈরি করে এবং বিদ্যমান থ্রেড পুনরায় ব্যবহার করে। সমস্যা: এটি থ্রেডের সর্বোচ্চ সংখ্যা সীমিত করে না। যদি একসাথে 100টি কাজ আসে, 100টি থ্রেড তৈরি হবে। এটি Android-এ OOM-এর কারণ হয় (প্রতি থ্রেড ~1 MB)। স্পষ্ট সীমা সহ newFixedThreadPool(n) ব্যবহার করুন। CachedThreadPool শুধুমাত্র গ্যারান্টিড ছোট ভলিউম সহ স্বল্পমেয়াদী burst কাজের জন্য গ্রহণযোগ্য।
হ্যাঁ, যদি পুল একটি পরিচালিত কন্টেইনার (যেমন করুটিন) এর অন্তর্গত না হয়। shutdown() নতুন কাজ গ্রহণ বন্ধ করে এবং বর্তমান কাজ সম্পূর্ণ করার পর থ্রেড শেষ করে। shutdown() ছাড়া, থ্রেড মেমোরিতে ঝুলে থাকে এবং অ্যাপ্লিকেশন শেষ হয় না। Activity-র জন্য, এটি onDestroy()-এ কল করুন। ViewModel-এর জন্য, coroutineScope ব্যবহার করুন। পুল বন্ধ করা রিসোর্স ব্যবস্থাপনার একটি বাধ্যতামূলক অংশ, Cursor বা InputStream বন্ধ করার মতো।
হ্যাঁ, OperationQueue এবং DispatchQueue iOS দ্বারা প্রদত্ত থ্রেড পুল। OperationQueue maxConcurrentOperationCount-এর মাধ্যমে concurrency সীমিত করে। DispatchQueue.global() সরাসরি নিয়ন্ত্রণ ছাড়া সিস্টেম থ্রেড পুল ব্যবহার করে। Java ThreadPoolExecutor-এর বিপরীতে, আপনি corePoolSize বা কিউ ক্ষমতা পরিচালনা করেন না — সিস্টেম বর্তমান লোড এবং ডিভাইস শক্তি খরচের উপর ভিত্তি করে পুল স্বয়ংক্রিয়ভাবে অপটিমাইজ করে।
সারসংক্ষেপ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন