Deadlock (পারস্পরিক ব্লকিং) এমন একটি অবস্থা যেখানে দুই বা ততোধিক থ্রেড অন্যান্য অংশগ্রহণকারীদের দ্বারা ধারণ করা সম্পদ মুক্ত করার জন্য অনির্দিষ্টকাল ধরে অপেক্ষা করে। Oracle Java Tutorials (2024) অনুসারে, Deadlock চক্রীয় অপেক্ষায় ঘটে যখন প্রতিটি থ্রেড একটি লক ধারণ করে যা অন্য থ্রেডের প্রয়োজন। বিশেষ সনাক্তকরণ টুল ছাড়া, Deadlock দৃশ্যমান ত্রুটি ছাড়াই অ্যাপ্লিকেশন নির্বাহ সম্পূর্ণরূপে বন্ধ করে দেয়।
মূল পয়েন্ট
Deadlock মাল্টিথ্রেডেড প্রোগ্রামিং-এ এমন একটি অবস্থা যেখানে দুই বা ততোধিক থ্রেড একে অপরকে স্থায়ীভাবে ব্লক করে। প্রতিটি থ্রেড অন্য থ্রেডের প্রয়োজনীয় একটি সম্পদ ধারণ করে এবং অনুপস্থিত সম্পদ অর্জনের অপেক্ষায় এটি ছেড়ে দেয় না। ফলস্বরূপ, কোনো থ্রেডই নির্বাহ চালিয়ে যেতে পারে না।
মোবাইল ডেভেলপমেন্টে, Deadlock বিশেষভাবে গুরুত্বপূর্ণ কারণ এটি ব্যতিক্রম বা ক্র্যাশ সৃষ্টি করে না। অ্যাপ্লিকেশন ব্যবহারকারীর ক্রিয়ায় সাড়া দেওয়া বন্ধ করে দেয় (ANR — Application Not Responding), এবং একমাত্র উপায় হল জোর করে প্রক্রিয়াটি শেষ করা। Google (Android Performance Patterns, 2023) অনুসারে, Google Play Console-এ প্রায় 15% ANR রিপোর্ট ব্যাকগ্রাউন্ড থ্রেডে পারস্পরিক ব্লকিংয়ের সাথে সম্পর্কিত।
Deadlock এবং অন্যান্য concurrency সমস্যার মধ্যে মূল পার্থক্য হল বাহ্যিক হস্তক্ষেপ ছাড়া এর অপরিবর্তনীয়তা। থ্রেড নিজেদের থেকে সম্পদ ছেড়ে দেবে না কারণ অপারেটিং সিস্টেম শিডিউলার জোর করে লক প্রত্যাহার করতে পারে না। এটি Deadlock-কে Livelock থেকে পৃথক করে, যেখানে থ্রেড সক্রিয় কিন্তু দরকারী কাজ করছে না।
1971 সালে, Edward G. Coffman Deadlock হওয়ার জন্য প্রয়োজনীয় চারটি বাধ্যতামূলক শর্ত প্রণয়ন করেন। যদি তাদের মধ্যে অন্তত একটি অনুপস্থিত থাকে, তাহলে পারস্পরিক ব্লকিং অসম্ভব। এই শর্তগুলি Coffman-এর শর্ত হিসাবে পরিচিত এবং সমস্ত Deadlock প্রতিরোধ অ্যালগরিদমের ভিত্তি তৈরি করে।
একটি সম্পদ যেকোনো সময়ে শুধুমাত্র একটি থ্রেড দ্বারা ধারণ করা যেতে পারে। যদি একটি সম্পদ একাধিক থ্রেড দ্বারা একসাথে পড়ার অনুমতি দেয় (যেমন ReadWriteLock রিড মোডে), তাহলে Deadlock ঘটে না। এই শর্তটি Mutex এবং লকের প্রকৃতি থেকে উদ্ভূত।
একটি থ্রেড ইতিমধ্যে অর্জিত সম্পদ ধারণ করে এবং একই সাথে অন্য একটি সম্পদ অর্জনের জন্য অপেক্ষা করে। যদি একটি থ্রেড পরবর্তী সম্পদ অনুরোধ করার আগে বর্তমান সম্পদ ছেড়ে দিতে পারে (দুই-ফেজ লকিংয়ের মাধ্যমে), তাহলে Hold and Wait শর্ত ভেঙে যায়। Android-এ, এটি প্রায়শই প্রকাশ পায় যখন একটি থ্রেড ডাটাবেস লক ধারণ করে এবং SharedPreferences লক অর্জনের চেষ্টা করে।
অপারেটিং সিস্টেম একটি থ্রেড থেকে জোর করে লক কেড়ে নিতে পারে না। সম্পদ তখনই মুক্ত হয় যখন থ্রেড নিজেই এটি ছেড়ে দেয়। কিছু সিস্টেমে (যেমন SQLite WAL মোড), পৃথক অপারেশনের স্তরে জোরপূর্বক অপসারণ প্রয়োগ করা হয়, যা Deadlock-এর ঝুঁকি হ্রাস করে।
থ্রেডের একটি বন্ধ শৃঙ্খল বিদ্যমান, যার প্রতিটি শৃঙ্খলে পরবর্তী থ্রেড দ্বারা ধারণ করা সম্পদের জন্য অপেক্ষা করে। উদাহরণস্বরূপ, থ্রেড A সম্পদ 1 ধারণ করে এবং সম্পদ 2-এর জন্য অপেক্ষা করে, থ্রেড B সম্পদ 2 ধারণ করে এবং সম্পদ 1-এর জন্য অপেক্ষা করে। এটি একমাত্র শর্ত যা একজন ডেভেলপার আর্কিটেকচারালি দূর করতে পারে — লক শ্রেণিবিন্যাসের মাধ্যমে। যদি সমস্ত থ্রেড কঠোরভাবে সংজ্ঞায়িত বৈশ্বিক ক্রমে সম্পদ অর্জন করে, তাহলে চক্র শারীরিকভাবে অসম্ভব।
বাস্তবে, Android অ্যাপ্লিকেশনে, Deadlock প্রায়শই বিভিন্ন স্তরের লকের অন্তর্নিহিত ছেদ এর কারণে ঘটে: ডাটাবেস লক (Room), SharedPreferences লক এবং মেমরিতে কালেকশন লক। এই প্রতিটি লক বিভিন্ন উপাদান দ্বারা পরিচালিত হয়, এবং অর্জন ক্রমের জন্য কেন্দ্রীভূত প্রোটোকল ছাড়া, ডেভেলপাররা অনিচ্ছাকৃতভাবে চক্র তৈরি করে।
আসুন পারস্পরিক ব্লকিংয়ের একটি ক্লাসিক উদাহরণ দেখি — দুটি থ্রেড ভিন্ন ক্রমে লক অর্জন করে। যদি প্রথম থ্রেড সম্পদ A লক করে এবং B অর্জনের চেষ্টা করে, এবং দ্বিতীয়টি B লক করে এবং A অর্জনের চেষ্টা করে, তাহলে Deadlock ঘটে।
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun operationA() {
synchronized(lockA) {
Thread.sleep(50) // কাজের সিমুলেশন
synchronized(lockB) {
println("operationA সম্পন্ন")
}
}
}
fun operationB() {
synchronized(lockB) { // বিপরীত ক্রম
Thread.sleep(50)
synchronized(lockA) {
println("operationB সম্পন্ন")
}
}
}
}
fun main() {
val ex = DeadlockExample()
Thread { ex.operationA() }.start()
Thread { ex.operationB() }.start()
// অ্যাপ্লিকেশন চিরকালের জন্য হ্যাং হবে — Deadlock!
}
এই উদাহরণে, operationA lockA অর্জন করে, এবং operationB lockB অর্জন করে। তারপর প্রতিটি দ্বিতীয় লক অর্জনের চেষ্টা করে — এবং উভয়ই অনির্দিষ্টকালের জন্য অপেক্ষা করে। প্রোগ্রাম ত্রুটি বার্তা ছাড়াই হ্যাং হয়ে যায়। এটি ঠিক করার একমাত্র উপায় হল সমস্ত পদ্ধতিতে একই লক অর্জনের ক্রম নিশ্চিত করা।
এই তিনটি concurrency সমস্যা প্রায়ই বিভ্রান্ত হয়, কিন্তু তাদের প্রক্রিয়া এবং ফলাফল মৌলিকভাবে ভিন্ন। Deadlock — সম্পূর্ণ বিরতি, Starvation — সম্পদের অসীম অপেক্ষা, Livelock — সক্রিয় নিষ্ক্রিয়তা। সঠিক সমাধান কৌশল বেছে নেওয়ার জন্য পার্থক্য বোঝা গুরুত্বপূর্ণ।
| বৈশিষ্ট্য | Deadlock | Starvation | Livelock |
|---|---|---|---|
| থ্রেডের অবস্থা | অবরুদ্ধ (BLOCKED) | প্রস্তুত (RUNNABLE) | সক্রিয় (RUNNABLE) |
| কাজ সম্পাদন | না | না | হ্যাঁ, কিন্তু অকেজো |
| কারণ | চক্রীয় অপেক্ষা | অন্যায্য শিডিউলিং | ভুল দ্বন্দ্ব ব্যবস্থাপনা |
| সনাক্তকরণ | Thread Dump, টাইমআউট | অগ্রগতি পর্যবেক্ষণ | পুনরায় চেষ্টা কাউন্টার |
Starvation (অনাহার) ঘটে যখন শিডিউলার অন্যদের পক্ষে নিম্ন-অগ্রাধিকার থ্রেডের নির্বাহ ক্রমাগত স্থগিত করে। Deadlock-এর বিপরীতে, থ্রেড অবরুদ্ধ নয় — এটি নির্বাহের জন্য প্রস্তুত কিন্তু CPU সময় পায় না। Android-এ, একটি সাধারণ পরিস্থিতি হল নিম্ন-অগ্রাধিকার ব্যাকগ্রাউন্ড থ্রেড যা কখনও নির্বাহিত হয় না যদি UI থ্রেড এবং Service থ্রেড ক্রমাগত সক্রিয় থাকে।
Livelock (সক্রিয় ব্লকিং) এমন একটি অবস্থা যেখানে থ্রেড অবরুদ্ধ নয় কিন্তু একে অপরের ক্রিয়ায় অসীমভাবে প্রতিক্রিয়া জানায় দরকারী কাজ না করে। ক্লাসিক উপমা — দুজন ব্যক্তি করিডোরে মিলিত হয় এবং উভয়েই পথ ছেড়ে দেওয়ার চেষ্টা করে, একই দিকে চলে। Deadlock-এর বিপরীতে, Livelock-এ থ্রেড CPU খরচ করে, ডিভাইসের ব্যাটারি নিষ্কাশন করে।
Thread Dump JVM এবং Android Runtime-এ পারস্পরিক ব্লকিং সনাক্তকরণের প্রাথমিক টুল। ডাম্পের সময়, JVM স্বয়ংক্রিয়ভাবে মনিটরের মধ্যে নির্ভরতা গ্রাফ বিশ্লেষণ করে এবং Deadlock চক্র চিহ্নিত করে। Android Studio-এ, থ্রেড ডাম্প Android Profiler বা ADB Shell থেকে kill -3 PID কমান্ডের মাধ্যমে পাওয়া যেতে পারে।
রানটাইমে স্বয়ংক্রিয় Deadlock সনাক্তকরণ Watchdog টাইমার এর মাধ্যমে বাস্তবায়িত হয়। যদি একটি থ্রেড নির্দিষ্ট টাইমআউটের মধ্যে একটি অপারেশন সম্পূর্ণ না করে, তাহলে watchdog একটি ডাম্প শুরু করে এবং Crash Reporting সিস্টেমে (Firebase Crashlytics, Sentry) একটি রিপোর্ট পাঠায়। Sentry (Issue Resolution Report, 2024) অনুসারে, watchdog কনফিগার করা Deadlock নির্ণয়ের সময় সপ্তাহ থেকে কয়েক ঘন্টায় কমিয়ে আনে।
ডেভেলপমেন্ট পর্যায়ে, JetBrains-এর ThreadSafe স্ট্যাটিক বিশ্লেষক এবং Lock Checker মডিউল সহ Checker Framework কার্যকর। এই টুলগুলি সোর্স কোড স্তরে লক অর্জনের ক্রম বিশ্লেষণ করে এবং সম্ভাব্য চক্র সম্পর্কে সতর্ক করে। অতিরিক্তভাবে, Test-Driven Deadlock Detection সুপারিশ করা হয় — স্ট্রেস টেস্ট যা শত শত থ্রেডে বিভিন্ন লক ক্রম সহ অপারেশন চালায়।
বিশেষ মনোযোগ প্রাপ্য Cooperative Deadlock Detection — একটি পদ্ধতি যেখানে থ্রেড একটি বৈশ্বিক রেজিস্ট্রির মাধ্যমে অর্জিত লক সম্পর্কে তথ্য বিনিময় করে। যদি একটি থ্রেড সম্ভাব্য চক্র সনাক্ত করে, এটি সমস্ত সম্পদ মুক্ত করে এবং অপারেশন পুনরায় চেষ্টা করে। এই পদ্ধতিটি বিতরণকৃত সিস্টেমে (Apache ZooKeeper, Google Chubby) ব্যবহৃত হয় এবং Jetpack Sync-এর মতো লাইব্রেরির মাধ্যমে ধীরে ধীরে মোবাইল ডেভেলপমেন্টে গৃহীত হচ্ছে।
সবচেয়ে নির্ভরযোগ্য উপায় হল সম্পূর্ণ অ্যাপ্লিকেশন জুড়ে লক অর্জনের বৈশ্বিক ক্রম প্রতিষ্ঠা করা। যদি সমস্ত থ্রেড সর্বদা প্রথমে ছোট নম্বরের লক অর্জন করে, তারপর বড় নম্বরেরটি, তাহলে চক্রীয় অপেক্ষা (Circular Wait শর্ত) অসম্ভব। বড় প্রকল্পে, ক্রমটি নথিভুক্ত করা হয় এবং কোড পর্যালোচনার মাধ্যমে যাচাই করা হয়।
TryLock একটি লকিং পদ্ধতি যা থ্রেডকে অনির্দিষ্টকালের জন্য ব্লক করে না, বরং নির্দিষ্ট সময়ের মধ্যে লক অর্জিত না হলে false ফেরত দেয়। Java-তে, এটি ReentrantLock.tryLock(timeout, TimeUnit) এর মাধ্যমে বাস্তবায়িত হয়, Kotlin Coroutines-এ — টাইমআউট সহ Mutex.withLock এর মাধ্যমে। ব্যর্থ হলে, থ্রেড সমস্ত অর্জিত সম্পদ মুক্ত করে এবং পরে পুনরায় চেষ্টা করে।
ব্যাংকার অ্যালগরিদম Edsger Dijkstra দ্বারা প্রস্তাবিত Deadlock প্রতিরোধের একটি তাত্ত্বিক পদ্ধতি। এটি সম্পদ বরাদ্দকে ব্যাংক লেনদেন হিসাবে মডেল করে: সিস্টেম একটি সম্পদ বরাদ্দ করে না যদি এটি একটি অনিরাপদ অবস্থার (deadlock) দিকে নিয়ে যেতে পারে। বাস্তবে, থ্রেডের সর্বোচ্চ প্রয়োজন আগে থেকে জানার অসুবিধার কারণে অ্যালগরিদম মোবাইল ডেভেলপমেন্টে খুব কমই ব্যবহৃত হয়, তবে এর নীতিগুলি SQLite ডাটাবেস এবং ফাইল সিস্টেমে ব্যবহৃত হয়।
প্রায়শই জিজ্ঞাসিত প্রশ্ন
না, পারস্পরিক ব্লকিংয়ের জন্য কমপক্ষে দুটি থ্রেড প্রয়োজন। একক-থ্রেডেড কোডে, সমস্ত অপারেশন ক্রমান্বয়ে নির্বাহিত হয়, তাই চক্রীয় অপেক্ষা অসম্ভব। তবে, ফাইল লক বা ইন্টারপ্রসেস সেমাফোর ব্যবহার করার সময় প্রক্রিয়াগুলির মধ্যে Deadlock হতে পারে।
Coroutines-এ, Deadlock সাসপেন্ড ফাংশন (suspend) এর স্তরে ঘটে এবং OS থ্রেড ব্লক করে না, যা এটিকে কম লক্ষণীয় করে তোলে। kotlinx.coroutines-এর Mutex একটি সাসপেন্ডিং লক — এটি থ্রেড ব্লক করে না, কিন্তু coroutine নির্বাহিত হয় না। সনাক্তকরণের জন্য, kotlinx-coroutines-debug মডিউল থেকে DebugProbes ব্যবহার করুন।
SQLite Deadlock ঘটে যখন ডাটাবেসের দুটি সংযোগ ভিন্ন ক্রমে লেনদেন নির্বাহ করার চেষ্টা করে। SQLite এই ধরনের পরিস্থিতি সনাক্ত করে এবং SQLITE_BUSY বা SQLITE_LOCKED ত্রুটি কোড ফেরত দেয়। Android-এ, একটি একক ডাটাবেস ইনস্ট্যান্স এবং @Transaction এর মাধ্যমে লেনদেন সহ Room ব্যবহার করার সুপারিশ করা হয়, যা আন্তঃ-সংযোগ Deadlock দূর করে।
Android Runtime-এ একটি অন্তর্নির্মিত Deadlock ডিটেক্টর রয়েছে যা ANR (Application Not Responding) উৎপন্ন হলে চলে। সিস্টেম অ্যাপ্লিকেশনের সমস্ত থ্রেডের Thread Dump বিশ্লেষণ করে এবং পারস্পরিক ব্লকিং চিহ্নিত করে। ফলাফল /data/anr/traces.txt এবং Google Play Console-এর ANR Reports বিভাগে উপলব্ধ।
প্রথমে, অ্যাপ্লিকেশনের সমস্ত থ্রেডের Thread Dump পান। বিশ্লেষণ করুন প্রতিটি থ্রেড কোন লক ধারণ করে এবং কোনটি অর্জনের চেষ্টা করছে। সময়সীমা অতিক্রান্ত হলে স্বয়ংক্রিয় ডাম্প সহ Watchdog টাইমার প্রয়োগ করুন। ঠিক করার পর, পুনরাবৃত্তি রোধ করতে CI পাইপলাইনে ThreadSafety lint নিয়ম যোগ করুন।
সারসংক্ষেপ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন