Starvation (থ্রেড স্টারভেশন) — এমন একটি পরিস্থিতি যেখানে একটি থ্রেড কাজ চালিয়ে যাওয়ার জন্য প্রয়োজনীয় সম্পদে অ্যাক্সেস পায় না, যদিও এটি সম্পাদনের জন্য প্রস্তুত। Baeldung (Java Thread Starvation, 2024) অনুসারে, অন্যায্য শিডিউলিংয়ের কারণে স্টারভেশন দেখা দেয়, যখন নিম্ন-অগ্রাধিকার থ্রেডগুলি উচ্চ-অগ্রাধিকার থ্রেডগুলির পক্ষে ক্রমাগত স্থগিত করা হয়। Deadlock-এর বিপরীতে, Starvation থ্রেডকে ব্লক করে না — এটি RUNNABLE অবস্থায় থাকে কিন্তু কখনও CPU সময় পায় না।
মূল বিষয়
Starvation (থ্রেড স্টারভেশন) — একটি মাল্টিথ্রেডিং সমস্যা যেখানে একটি থ্রেড তার কাজ সম্পূর্ণ করার জন্য প্রয়োজনীয় সম্পদে অ্যাক্সেস পায় না, যদিও সম্পদটি অন্য থ্রেড দ্বারা স্থায়ীভাবে লক করা না থাকে। থ্রেডটি RUNNABLE অবস্থায় থাকে, কিন্তু শিডিউলার বা সিঙ্ক্রোনাইজেশন মেকানিজম পদ্ধতিগতভাবে অন্যান্য থ্রেডের পক্ষে এর সম্পাদন স্থগিত করে।
মোবাইল ডেভেলপমেন্টে, Starvation অসম কার্য সম্পাদন হিসেবে প্রকাশ পায়: কিছু অপারেশন তাত্ক্ষণিকভাবে সম্পাদিত হয়, অন্যগুলি বিপর্যয়কর বিলম্বে ভোগে। উদাহরণস্বরূপ, একটি ব্যাকগ্রাউন্ড ডেটা সিঙ্ক্রোনাইজেশন থ্রেড কখনই ডাটাবেসে অ্যাক্সেস পেতে পারে না যদি UI থ্রেড এবং অ্যানিমেশন হ্যান্ডলারগুলি ক্রমাগত এটিকে অতিক্রম করে। Android Developer Blog (Performance Matters, 2023) অনুসারে, Android-এ প্রায় 12% মিসড ফ্রেম (jank) রেন্ডারিং-এর উপর নির্ভরশীল ব্যাকগ্রাউন্ড কাজের Starvation-এর কারণে ঘটে।
Starvation এবং Deadlock-এর মধ্যে মূল পার্থক্য হল প্রত্যাবর্তনযোগ্যতা। যদি সিস্টেম লোড কমে যায় বা অগ্রাধিকার পুনর্বণ্টন করা হয়, তবে ক্ষুধার্ত থ্রেড সম্পদ অর্জন করতে পারে এবং তার কাজ সম্পূর্ণ করতে পারে। তবে, ক্রমাগত উচ্চ লোডের অধীনে, Starvation অনির্দিষ্টকাল ধরে চলতে পারে, যা একটি হিমায়িত অ্যাপ্লিকেশনের ধারণা তৈরি করে।
Java এবং Kotlin-এ synchronized অন্যায্য প্রক্রিয়ার একটি সর্বোত্তম উদাহরণ। উচ্চ প্রতিযোগিতার অধীনে, JVM একই সক্রিয় থ্রেডগুলিকে ক্রমাগত লক প্রদান করতে পারে, অন্যদিকে অন্যান্য থ্রেড ক্রমাগত প্রতিযোগিতায় হেরে যায়। এটি JVM-এর বাগ নয় বরং একটি ডিজাইন ট্রেড-অফ: অন্যায্য লকগুলি অ্যাক্সেসের ন্যায্যতার ব্যয়ে উচ্চতর থ্রুপুট প্রদান করে। ৪–৮ থ্রেড বিশিষ্ট মোবাইল অ্যাপ্লিকেশনের জন্য, এই সমস্যা বিশেষভাবে প্রাসঙ্গিক।
ভিন্ন থ্রেড অগ্রাধিকার সেট করা নিম্ন-অগ্রাধিকার থ্রেডের Starvation ঘটাতে পারে। Android Runtime-এ, Linux CFS (Completely Fair Scheduler) শিডিউলার CPU সময় অগ্রাধিকারের অনুপাতে বিতরণ করে, এবং যদি উচ্চ-অগ্রাধিকার থ্রেডগুলি ক্রমাগত সক্রিয় থাকে, নিম্ন-অগ্রাধিকার থ্রেডগুলি কখনই CPU সময় নাও পেতে পারে। Google Android-এ থ্রেড অগ্রাধিকার পরিবর্তন করতে কঠোরভাবে নিরুৎসাহিত করে — সিস্টেম নিজেই সেগুলি পরিচালনা করে।
যদি একটি থ্রেড অত্যধিক সময় ধরে লক ধরে রাখে (synchronized ব্লকের ভিতরে ভারী গণনা, নেটওয়ার্ক অনুরোধ বা ফাইল অপারেশন সম্পাদন করে), সেই লকের জন্য অপেক্ষারত অন্যান্য থ্রেড ক্ষুধার্ত হয়। এটি Android-এ বিশেষভাবে বিপজ্জনক, যেখানে UI থ্রেডে দীর্ঘ অপারেশন ANR সৃষ্টি করে, এবং ক্রিটিকাল সেকশন অপ্টিমাইজ না করে সেগুলিকে ব্যাকগ্রাউন্ড থ্রেডে সরানো কেবল Starvation সমস্যাকে ওয়ার্কার থ্রেডে স্থানান্তর করে।
এমন একটি উদাহরণ বিবেচনা করুন যেখানে অন্যায্য শিডিউলিংয়ের কারণে একটি থ্রেড খুব ঘন ঘন লক অর্জন করে। Starvation একটি উচ্চ-অগ্রাধিকার থ্রেডের অসীম লুপের মাধ্যমে প্রদর্শিত হয় যা একটি নিম্ন-অগ্রাধিকার থ্রেডকে ভাগ করা সম্পদে অ্যাক্সেস করতে বাধা দেয়।
class SharedResource {
private val lock = Any()
fun criticalSection(id: String) {
synchronized(lock) {
println("$id অ্যাক্সেস পেয়েছে")
Thread.sleep(10) // কাজের সিমুলেশন
}
}
}
fun main() {
val resource = SharedResource()
// উচ্চ-অগ্রাধিকার থ্রেড — ক্রমাগত সক্রিয়
val highPriority = Thread {
while (true) {
resource.criticalSection("High")
}
}
// নিম্ন-অগ্রাধিকার থ্রেড — কখনও অ্যাক্সেস নাও পেতে পারে
val lowPriority = Thread {
while (true) {
resource.criticalSection("Low")
}
}
highPriority.start()
lowPriority.start()
// "Low" কখনও বার্তা প্রিন্ট নাও করতে পারে — Starvation!
}
এই উদাহরণে, highPriority থ্রেড ক্রমাগত লক অর্জন করে এবং মাত্র 10 ms-এর জন্য এটি ছেড়ে দেয়। synchronized-এর অন্যায্য প্রকৃতির কারণে, JVM শিডিউলার সম্ভবত লকটি আবার সেই থ্রেডকেই প্রদান করবে যে এইমাত্র এটি ছেড়েছে — নিম্ন-অগ্রাধিকার থ্রেড ক্ষুধার্ত হয়। সমাধান হল fair ফ্ল্যাগ সহ ReentrantLock(true) ব্যবহার করা, যা FIFO অপেক্ষা ক্রম নিশ্চিত করে।
ফেয়ার লক সহ সংশোধিত সংস্করণ সম্পদে ন্যায্য অ্যাক্সেস নিশ্চিত করে।
class FairSharedResource {
private val fairLock = ReentrantLock(true) // fair = true
fun criticalSection(id: String) {
fairLock.lock()
try {
println("$id অ্যাক্সেস পেয়েছে (fair)")
Thread.sleep(10)
} finally {
fairLock.unlock()
}
}
}
তিনটি ক্লাসিক মাল্টিথ্রেডিং সমস্যা — Starvation, Deadlock এবং Livelock — প্রায়শই একসাথে গ্রুপ করা হয়, তবে তাদের প্রক্রিয়া এবং সমাধান ভিন্ন। Starvation — থ্রেড প্রস্তুত কিন্তু সম্পদ অর্জন করতে পারে না। Deadlock — থ্রেড চক্রীয় অপেক্ষায় ব্লকড। Livelock — থ্রেড সক্রিয় কিন্তু অগ্রগতি হয় না।
| প্যারামিটার | Starvation | Deadlock | Livelock |
|---|---|---|---|
| থ্রেড অবস্থা | RUNNABLE | BLOCKED | RUNNABLE |
| অগ্রগতি | কোনোটিই নয় | কোনোটিই নয় | কোনোটিই নয় (যদিও সক্রিয়) |
| CPU ব্যবহার | কম | সর্বনিম্ন | উচ্চ (100% পর্যন্ত) |
| কারণ | অন্যায্য শিডিউলিং | চক্রীয় অপেক্ষা | দ্বন্দ্বে একই প্রতিক্রিয়া |
| প্রধান সমাধান | Fair Lock, ক্রিটিকাল সেকশন ছোট করা | লক শ্রেণিবিন্যাস | পুনরায় চেষ্টা সীমা, exponential backoff |
Starvation Deadlock-এর চেয়ে কম গুরুতর বলে বিবেচিত হয় কারণ এটি মারাত্মক নয় — কম লোডে, ক্ষুধার্ত থ্রেড শেষ পর্যন্ত সম্পাদিত হবে। তবে, বাস্তব Android ব্যবহারে, যেখানে মেমরি এবং CPU সীমিত, Starvation মিনিট ধরে চলতে পারে, যা একটি অগ্রহণযোগ্য UX তৈরি করে।
Thread Dump ছোট ব্যবধানে বারবার নেওয়া — স্টারভেশন সনাক্তকরণের মৌলিক পদ্ধতি। যদি একটি থ্রেড ধারাবাহিকভাবে RUNNABLE অবস্থায় থাকে কিন্তু একাধিক ডাম্প জুড়ে তার কল স্ট্যাক পরিবর্তন না হয় — এটি Starvation-এর একটি ক্লাসিক লক্ষণ। Android Studio-এ, সময়ের সাথে থ্রেড অবস্থা রেকর্ডিংয়ের জন্য Android Profiler ব্যবহার করুন।
কাজের সম্পাদন সময় পর্যবেক্ষণের মাধ্যমে স্বয়ংক্রিয় সনাক্তকরণ সম্ভব। যদি একটি পূর্বানুমানযোগ্য সম্পাদন সময় (যেমন, 50 ms) বিশিষ্ট কাজ 5 সেকেন্ড বা তার বেশি সময় নেয় — Starvation-এর উচ্চ সম্ভাবনা রয়েছে। মোবাইল অ্যাপ্লিকেশনে, Firebase Performance Monitoring আপনাকে ক্রিটিকাল সেকশনের জন্য কাস্টম ট্রেস সেটআপ করতে এবং থ্রেশহোল্ড অতিক্রম করলে বিজ্ঞপ্তি পেতে দেয়।
synchronized ব্লকের কারণে Starvation নির্ণয়ের জন্য, Java Flight Recorder (JFR) (OpenJDK API-এর মাধ্যমে Android-এ উপলব্ধ) বা Async Profiler ব্যবহার করুন। এই সরঞ্জামগুলি দেখায় কোন মনিটরগুলির সবচেয়ে বেশি অপেক্ষার সময় রয়েছে এবং কোন থ্রেডগুলি প্রতিটি মনিটরের জন্য প্রতিযোগিতা করছে। JFR ডেটা তার বিল্ট-ইন প্রোফাইলারের মাধ্যমে IntelliJ IDEA Ultimate-এর সাথে একীভূত হয়।
ReentrantLock(true) নিশ্চিত করে যে থ্রেডগুলি FIFO ক্রমে লক অর্জন করে। synchronized-এর বিপরীতে, একটি ফেয়ার লক সেই থ্রেডকে অবিলম্বে লক পুনরায় অর্জনের অনুমতি দেয় না যে এইমাত্র এটি ছেড়েছে। এটি Starvation সম্পূর্ণরূপে দূর করে, যদিও সারি বজায় রাখার ওভারহেডের কারণে সামগ্রিক থ্রুপুট ১০–২০% কমে যায়।
লক-মুক্ত ডেটা কাঠামো (ConcurrentHashMap, AtomicReference, LongAdder) সংজ্ঞা অনুসারে Starvation দূর করে, কারণ তাদের মধ্যে এমন লক নেই যা একটি থ্রেড ধরে রাখতে পারে। সমস্ত অপারেশন CPU CAS নির্দেশ ব্যবহার করে যা সীমিত সংখ্যক ধাপে কমপক্ষে একটি থ্রেডের অগ্রগতি নিশ্চিত করে। মোবাইল ডেভেলপমেন্টের জন্য, কাজের সারির জন্য ConcurrentLinkedQueue পছন্দ করুন।
লক ধারণ সময় কমানো Starvation-এর ঝুঁকি কমানোর একটি সার্বজনীন উপায়। ভারী অপারেশন (নেটওয়ার্ক, ডিস্ক I/O, জটিল গণনা) synchronized ব্লকের বাইরে সরান। ReadWriteLock ব্যবহার করুন সেই পরিস্থিতিতে যেখানে পাঠকদের বিরল লেখকদের কারণে ক্ষুধার্ত হওয়া উচিত নয়। Kotlin Coroutines লাইব্রেরি একটি suspending প্রক্রিয়া সহ Mutex প্রদান করে যা OS থ্রেডকে ব্লক করে না।
Condition.await() এবং signal() সতর্কতার সাথে ব্যবহার করা উচিত: একটি Condition-এ অপেক্ষারত থ্রেড অন্যান্য থ্রেডের সাথে জেগে ওঠে (spurious wakeup), এবং সবাই লকের জন্য প্রতিযোগিতা করে। যদি একটি থ্রেড await-এর পরপরই অপেক্ষায় ফিরে আসে যখন অন্যেরা লক অর্জনে সফল হয়, ক্ষুধার্ত থ্রেড অনির্দিষ্টকাল ধরে জেগে ও ঘুমাতে পারে। পুনরায় পরীক্ষা নিশ্চিত করতে সর্বদা if-এর পরিবর্তে while লুপে শর্ত পরীক্ষা করুন।
প্রায়শই জিজ্ঞাসিত প্রশ্ন
Priority Inversion — এমন একটি পরিস্থিতি যেখানে একটি নিম্ন-অগ্রাধিকার থ্রেড এমন একটি লক ধরে রাখে যা একটি উচ্চ-অগ্রাধিকার থ্রেডের প্রয়োজন। ফলস্বরূপ, উচ্চ-অগ্রাধিকার থ্রেড নিম্ন-অগ্রাধিকার থ্রেডের জন্য অপেক্ষা করে — অগ্রাধিকারগুলি উল্টে যায়। Starvation একটি বিস্তৃত সমস্যা: থ্রেড অগ্রাধিকার নির্বিশেষে, অন্যায্য শিডিউলিং বা দীর্ঘ ক্রিটিকাল সেকশনের কারণে সম্পদ অর্জন করতে পারে না।
না, Starvation একটি মাল্টিথ্রেডিং সমস্যা। একক-থ্রেড কোডে সম্পদ প্রতিযোগিতা বা থ্রেড শিডিউলিং নেই। তবে, অ্যাসিঙ্ক্রোনাস একক-থ্রেড কোডে (যেমন, JavaScript ইভেন্ট লুপ) Starvation ঘটতে পারে যদি একটি মাইক্রোটাস্ক শূন্য বিলম্বে setTimeout-এর মাধ্যমে অন্যদের সম্পাদন অনির্দিষ্টকাল ধরে স্থগিত করে।
JMM (Java Memory Model) থ্রেডগুলির মধ্যে পরিবর্তনের দৃশ্যমানতার নিয়ম সংজ্ঞায়িত করে কিন্তু ন্যায্য শিডিউলিংয়ের গ্যারান্টি দেয় না। synchronized, JMM অনুসারে, সিকোয়েন্সিয়াল কনসিস্টেন্সি — মৌলিক সঠিকতা — নিশ্চিত করে, কিন্তু Starvation প্রতিরোধ করে না। ন্যায্যতার জন্য JMM-এ উল্লিখিত নয় এমন অতিরিক্ত প্রক্রিয়া প্রয়োজন।
UI থ্রেড (Main Thread) শাস্ত্রীয় অর্থে ক্ষুধার্ত হতে পারে না কারণ এটির সর্বোচ্চ অগ্রাধিকার রয়েছে। তবে, Starvation ঘটে যখন UI থ্রেড একটি ক্ষুধার্ত ব্যাকগ্রাউন্ড থ্রেডের ফলাফলের জন্য অপেক্ষা করে। একটি সাধারণ পরিস্থিতি: একটি AsyncTask বা করুটিন ডেটা লোড করে কিন্তু অন্যান্য থ্রেডের সাথে প্রতিযোগিতার কারণে ডাটাবেসে অ্যাক্সেস পেতে পারে না, এবং UI অপেক্ষায় হিমায়িত হয়।
করুটিনে, Starvation প্রতিরোধ করতে, থ্রেড নিঃশেষ এড়াতে Dispatchers.IO-তে limitedParallelism ব্যবহার করুন। সিঙ্ক্রোনাইজেশনের জন্য, kotlinx.coroutines.sync থেকে Mutex ব্যবহার করুন — এটি থ্রেড ব্লক না করে করুটিনকে সাসপেন্ড করে, যা স্টারভেশনের ঝুঁকি হ্রাস করে। করুটিনে runBlocking এড়িয়ে চলুন, কারণ এটি একটি পুল থ্রেড দখল করতে পারে এবং অন্যান্য করুটিনের Starvation ঘটাতে পারে।
সারসংক্ষেপ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন