Deadlock (قفل متقابل) — وضعیتی است که دو یا چند ترد بطور نامتناهی منتظر آزادشدن منابعی هستند که توسط سایر شرکتکنندگان اشغال شدهاند. به استناد به Oracle Java Tutorials (2024)، Deadlock در انتظار چرخشی ایجاد میشود زمانی که هر ترد قفلی را نگه میدارد که برای ترد دیگر لازم است. بدون ابزارهای خاص آشکارسازی، Deadlock اجرای برنامه را بدون خطاهای قابل مشاهده بطور کامل متوقف میکند.
نکات کلیدی
Deadlock (قفل متقابل) — وضعیتی در برنامهنویسی چندرشتهای است که دو یا چند ترد یکدیگر را بطور دائم مسدود میکنند. هر ترد منبعی را که برای ترد دیگر لازم است نگه میدارد و آن را آزاد نمیکند، چرا که منتظر تصرف منبع گمشده است. در نتیجه، هیچکدام از تردها نمیتوانند به اجرا ادامه دهند.
در توسعه موبایل، Deadlock بویژه بحرانی است زیرا باعث ایجاد استثنا یا کراش نمیشود. برنامه صرفاً از پاسخگویی به اعمال کاربر باز میایستد (ANR — Application Not Responding) و تنها راه خروج، پایان اجباری فرآیند است. بر اساس دادههای Google (Android Performance Patterns, 2023)، حدود 15% گزارشهای ANR در Google Play Console با قفلهای متقابل در تردهای پسزمینه مرتبط است.
تفاوت کلیدی Deadlock از سایر مشکلات همزمانی — بازگشتناپذیری آن بدون دخالت خارجی است. تردها منابع را خودبهخود آزاد نمیکنند، زیرا زمانبندی سیستمعامل نمیتواند قفل را بزور پس بگیرد. این Deadlock را از Livelock متمایز میکند، جایی که تردها فعال هستند اما کار مفیدی انجام نمیدهند.
در سال 1971، Edward G. Coffman چهار شرط الزامی برای بروز Deadlock را فرمولبندی کرد. اگر حتی یکی از آنها وجود نداشته باشد، قفل متقابل غیرممکن است. این شرایط بعنوان شرایط کافمن شناخته میشوند و اساس تمامی الگوریتمهای جلوگیری از Deadlock هستند.
منبع میتواند در هر لحظه تنها توسط یک ترد تصرف شود. اگر منبع خواندن همزمان توسط چند ترد را مجاز میدهد (مثلاً، ReadWriteLock در حالت خواندن)، Deadlock ایجاد نمیشود. این شرط از ماهیت Mutex و قفلها ناشی میشود.
یک ترد منبعی را که از قبل تصرف کرده نگه میدارد و همزمان منتظر تصرف منبع دیگری میماند. اگر ترد بتواند منبع فعلی را قبل از درخواست منبع بعدی آزاد کند (از طریق قفل دو فازی)، شرط Hold and Wait نقض میشود. در Android، این مسئله زمانی بروز میکند که یک ترد قفل پایگاه داده را نگه میدارد و سعی میکند قفل SharedPreferences را بدست آورد.
سیستمعامل نمیتواند قفل را اجباریاً از ترد پس بگیرد. منبع تنها زمانی آزاد میشود که خود ترد آن را رها کند. در برخی سیستمها (مثل حالت WAL SQLite)، تصادفی اجباری در سطح عملیاتهای فردی پیادهسازی شده است که خطر 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 را. سپس هر یک سعی میکند قفل دوم را تصرف کند — و هر دو بطور نامتناهی منتظر میمانند. برنامه بدون پیام خطا قطع میکند. تنها راه رفع مشکل، تضمین ترتیب یکسان تصرف قفلها در تمامی روشها است.
این سه مشکل همزمانی اغالب اشتباه گرفته میشوند، اما مکانیسمها و پیامدهای آنها اصولاً متفاوت است. Deadlock — توقف کامل، Starvation — انتظار نامتناهی برای منبع، Livelock — بیکاری فعال. درک تفاوتها برای انتخاب راهبرد صحیح رفع مشکل حیاتی است.
| ویژگی | Deadlock | Starvation | Livelock |
|---|---|---|---|
| وضعیت تردها | مسدود (BLOCKED) | آماده (RUNNABLE) | فعال (RUNNABLE) |
| اجرای کار | خیر | خیر | بله، اما بیفایده |
| علت | انتظار چرخشی | زمانبندی ناعادلانه | برخورد نادرست با تعارض |
| کشف | Thread Dump، مهلت زمان | نظارت بر پیشرفت | شمارنده تلاشهای مجدد |
Starvation (گرسنگی) زمانی ایجاد میشود که زمانبندی اجرای یک ترد کماولویت را بطور مداوم بنفع دیگران بتأخیر بیندازد. برخلاف Deadlock، ترد مسدود نیست — آماده اجراست اما زمان پردازنده دریافت نمیکند. در Android، سناریوی تیپیک زمانی است که ترد زمینه با اولویت پایین هرگز اجرا نمیشود اگر تردهای UI و Service مداوماً فعال باشند.
Livelock (قفل فعال) — وضعیتی است که تردها مسدود نیستند، اما بطور نامتناهی به اعمال یکدیگر واکنش نشان میدهند، بدون انجام کار مفید. قیاس کلاسیک — دو نفر در راهرو با یکدیگر برخورد میکنند و هر دو سعی میکنند راه را بدهند، در حالی که بیک جهت حرکت میکنند. برخلاف Deadlock، تردها در Livelock مصرف CPU دارند و باتری دستگاه را تخلیه میکنند.
Thread Dump (ریزش تردها) — ابزار اصلی کشف قفلهای متقابل در JVM و Android Runtime است. در زمان ریزش، JVM بطور خودکار گراف وابستگی بین مانیتورها را تجزیه و تحلیل میکند و چرخشهای Deadlock را علامتگذاری میکند. در Android Studio، ریزش تردها را میتوان از طریق Android Profiler یا دستور kill -3 PID از ADB Shell بدست آورد.
کشف خودکار Deadlock در زمان اجرا از طریق زمانسنجهای Watchdog انجام میشود. اگر یک ترد عملیات را در زمان مشخص تکمیل نکند، watchdog ریزش را آغاز کرده و گزارشی به سیستم گزارش کراش (Firebase Crashlytics، Sentry) ارسال میکند. بر اساس دادههای Sentry (Issue Resolution Report, 2024)، پیکربندی watchdog زمان تشخیص Deadlock را از هفتهها به چند ساعت کاهش میدهد.
در مرحله توسعه، تحلیلگر ایستا ThreadSafe از JetBrains و Checker Framework با ماژول Lock Checker موثر هستند. این ابزارها ترتیب تصرف قفلها را در سطح کد مبدأ تجزیه و تحلیل کرده و درباره چرخشهای پتانسیل هشدار میدهند. بعلاوه، 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 با مهلت زمان پیادهسازی شده است. در صورت شکست، ترد همه منابع تصرف شده را آزاد کرده و تلاش را بعداً تکرار میکند.
الگوریتم بانکدار — روش نظری جلوگیری از Deadlock است که توسط Edsger Dijkstra ارائه شده. آن توزیع منابع را بعنوان تراکنشهای بانکی مدلسازی میکند: سیستم منبعی را تخصیص نمیدهد اگر این بتواند به وضعیت ناامن (deadlock) منجر شود. در عمل، الگوریتم بندرت در توسعه موبایل استفاده میشود چون دانستن پیشاپیش حداکثر نیازهای تردها پیچیده است، اما اصول آن در پایگاههای داده SQLite و سیستمهای فایلی استفاده میشود.
پرسشهای متداول
خیر، برای قفل متقابل حداقل دو ترد لازم است. در کد تکرشتهای، تمامی عملیاتها بطور متوالی انجام میشوند، بنابراین انتظار چرخشی غیرممکن است. اما Deadlock میتواند بین فرآیندها با استفاده از قفلهای فایل یا سمافورهای بین فرآیندی ایجاد شود.
در کوروتینها، Deadlock در سطح توابع معلق (suspend) ایجاد میشود و ترد سیستمعامل را مسدود نمیکند، که آن را کمتر قابل توجه میکند. Mutex از kotlinx.coroutines معلقکننده (suspending) است، ترد را مسدود نمیکند، اما کوروتین اجرا نمیشود. برای کشف، از DebugProbes از ماژول kotlinx-coroutines-debug استفاده کنید.
Deadlock در SQLite زمانی ایجاد میشود که دو اتصال به پایگاه داده سعی میکنند تراکنشها را در ترتیبهای مختلف انجام دهند. SQLite چنین موقعیتهایی را کشف کرده و کد خطای SQLITE_BUSY یا SQLITE_LOCKED را برمیگرداند. در Android، استفاده از Room با یک نمونه تک پایگاه داده و تراکنشها از طریق @Transaction توصیه میشود که Deadlock میان اتصالی را از بین میبرد.
Android Runtime یک آشکارساز داخلی Deadlock دارد که در زمان تولید ANR (Application Not Responding) اجرا میشود. سیستم Thread Dump همه تردهای برنامه را تجزیه و تحلیل کرده و قفلهای متقابل را علامتگذاری میکند. نتیجه در /data/anr/traces.txt و Google Play Console در بخش ANR Reports قابل دسترسی است.
اولاً، Thread Dump همه تردهای برنامه را بدست آورید. تجزیه و تحلیل کنید که هر ترد چه قفلهایی را نگه میدارد و کدام را سعی میکند تصرف کند. یک زمانسنج Watchdog با ریزش خودکار در صورت تجاوز از محدودیت زمان پیادهسازی کنید. پس از رفع، یک قانون lint ThreadSafety را در خط لوله CI برای جلوگیری از عود اضافه کنید.
نتیجهگیری
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید