Deadlock (karşılıklı kilitleme), iki veya daha fazla iş parçacığının diğer katılımcılar tarafından tutulan kaynakların serbest bırakılmasını sonsuza kadar beklediği bir durumdur. Oracle Java Tutorials (2024)'e göre Deadlock, her iş parçacığının başka bir iş parçacığının ihtiyaç duyduğu bir kilidi tuttuğu döngüsel bekleme sırasında oluşur. Özel tespit araçları olmadan, Deadlock uygulama yürütmesini görünür hatalar olmadan tamamen durdurur.
Önemli Noktalar
Deadlock, çok iş parçacıklı programlamada iki veya daha fazla iş parçacığının birbirini kalıcı olarak bloke ettiği bir durumdur. Her iş parçacığı, başka bir iş parçacığı için gerekli olan bir kaynağı tutar ve eksik kaynağı edinmeyi beklerken onu serbest bırakmaz. Sonuç olarak, iş parçacıklarının hiçbiri yürütmeye devam edemez.
Mobil geliştirmede Deadlock özellikle kritiktir çünkü istisnalara veya çökmelere neden olmaz. Uygulama, kullanıcı eylemlerine yanıt vermeyi durdurur (ANR — Application Not Responding) ve tek çıkış yolu işlemi zorla sonlandırmaktır. Google'a göre (Android Performance Patterns, 2023), Google Play Console'daki ANR raporlarının yaklaşık %15'i arka plan iş parçacıklarındaki karşılıklı kilitlemelerle ilgilidir.
Deadlock ile diğer eşzamanlılık sorunları arasındaki temel fark, dış müdahale olmadan geri döndürülemez olmasıdır. İş parçacıkları kaynakları kendi başlarına serbest bırakmaz çünkü işletim sistemi zamanlayıcısı bir kilidi zorla geri alamaz. Bu, Deadlock'u, iş parçacıklarının aktif olduğu ancak yararlı iş yapmadığı Livelock'tan ayırır.
1971'de Edward G. Coffman, Deadlock'un oluşması için gerekli olan dört zorunlu koşulu formüle etti. Bunlardan en az biri yoksa, karşılıklı kilitleme imkansızdır. Bu koşullar Coffman koşulları olarak bilinir ve tüm Deadlock önleme algoritmalarının temelini oluşturur.
Bir kaynak, herhangi bir anda yalnızca bir iş parçacığı tarafından edinilebilir. Bir kaynak birden fazla iş parçacığı tarafından eşzamanlı okumaya izin veriyorsa (örneğin, okuma modunda ReadWriteLock), Deadlock oluşmaz. Bu koşul, Mutex ve kilitlerin doğasından kaynaklanır.
Bir iş parçacığı zaten edinilmiş bir kaynağı tutar ve aynı anda başka bir kaynağı edinmeyi bekler. Bir iş parçacığı, sonraki kaynağı talep etmeden önce mevcut kaynağı serbest bırakabiliyorsa (iki aşamalı kilitleme yoluyla), Hold and Wait koşulu bozulur. Android'de bu, genellikle bir iş parçacığının veritabanı kilidini tutması ve SharedPreferences kilidini edinmeye çalışmasıyla ortaya çıkar.
İşletim sistemi, bir iş parçacığından zorla bir kilidi alamaz. Kaynak yalnızca iş parçacığı onu serbest bıraktığında serbest kalır. Bazı sistemlerde (örneğin, SQLite WAL modu), tek tek işlemler düzeyinde zorla ele geçirme uygulanır ve bu da Deadlock riskini azaltır.
Her biri zincirdeki bir sonraki iş parçacığı tarafından tutulan bir kaynağı bekleyen kapalı bir iş parçacığı zinciri vardır. Örneğin, iş parçacığı A kaynak 1'i tutar ve kaynak 2'yi bekler, iş parçacığı B kaynak 2'yi tutar ve kaynak 1'i bekler. Bu, bir geliştiricinin mimari olarak ortadan kaldırabileceği tek koşuldur — bir kilit hiyerarşisi aracılığıyla. Tüm iş parçacıkları kaynakları kesin olarak tanımlanmış bir küresel sırayla edinirse, döngü fiziksel olarak imkansızdır.
Pratikte, Android uygulamalarında Deadlock en sık farklı seviyelerdeki kilitlerin örtük kesişmesi nedeniyle oluşur: veritabanı kilidi (Room), SharedPreferences kilidi ve bellekteki koleksiyon kilidi. Bu kilitlerin her biri farklı bileşenler tarafından yönetilir ve edinme sırası için merkezi bir protokol olmadan geliştiriciler istemeden döngüler oluşturur.
Karşılıklı kilitlemenin klasik bir örneğini ele alalım — iki iş parçacığı farklı sırayla kilit edinir. İlk iş parçacığı kaynak A'yı kilitler ve B'yi edinmeye çalışırsa, ikincisi B'yi kilitler ve A'yı edinmeye çalışırsa Deadlock oluşur.
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun operationA() {
synchronized(lockA) {
Thread.sleep(50) // iş simülasyonu
synchronized(lockB) {
println("operationA tamamlandı")
}
}
}
fun operationB() {
synchronized(lockB) { // ters sıra
Thread.sleep(50)
synchronized(lockA) {
println("operationB tamamlandı")
}
}
}
}
fun main() {
val ex = DeadlockExample()
Thread { ex.operationA() }.start()
Thread { ex.operationB() }.start()
// Uygulama sonsuza kadar donacak — Deadlock!
}
Bu örnekte, operationA lockA'yı edinir ve operationB lockB'yi edinir. Ardından her biri ikinci kilidi edinmeye çalışır — ve her ikisi de sonsuza kadar bekler. Program hata mesajı olmadan donar. Düzeltmenin tek yolu, tüm yöntemlerde aynı kilit edinme sırasını garanti etmektir.
Bu üç eşzamanlılık sorunu sıklıkla karıştırılır, ancak mekanizmaları ve sonuçları temelde farklıdır. Deadlock — tam durma, Starvation — bir kaynağın sonsuz beklemesi, Livelock — aktif hareketsizlik. Farklılıkları anlamak, doğru çözüm stratejisini seçmek için kritik öneme sahiptir.
| Özellik | Deadlock | Starvation | Livelock |
|---|---|---|---|
| İş parçacığı durumu | Bloke (BLOCKED) | Hazır (RUNNABLE) | Aktif (RUNNABLE) |
| İş yürütme | Hayır | Hayır | Evet, ancak yararsız |
| Neden | Döngüsel bekleme | Adaletsiz zamanlama | Yanlış çakışma işleme |
| Tespit | Thread Dump, zaman aşımı | İlerleme izleme | Yeniden deneme sayacı |
Starvation (açlık), zamanlayıcının düşük öncelikli bir iş parçacığının yürütmesini diğerleri lehine sürekli ertelemesiyle oluşur. Deadlock'un aksine, iş parçacığı bloke değildir — yürütmeye hazırdır ancak CPU zamanı alamaz. Android'de tipik bir senaryo, UI iş parçacığı ve Service iş parçacıkları sürekli aktifse düşük öncelikli bir arka plan iş parçacığının asla yürütülmemesidir.
Livelock (aktif kilit), iş parçacıklarının bloke olmadığı ancak yararlı iş yapmadan birbirlerinin eylemlerine sonsuz tepki verdiği bir durumdur. Klasik benzetme — iki kişinin bir koridorda karşılaşması ve her ikisinin de aynı yönde hareket ederek yol vermeye çalışması. Deadlock'un aksine, Livelock'taki iş parçacıkları CPU tüketir ve cihazın pilini boşaltır.
Thread Dump, JVM ve Android Runtime'da karşılıklı kilitlemeyi tespit etmek için birincil araçtır. Bir döküm sırasında JVM, monitörler arasındaki bağımlılık grafiğini otomatik olarak analiz eder ve Deadlock döngülerini işaretler. Android Studio'da, iş parçacığı dökümleri Android Profiler veya ADB Shell'den kill -3 PID komutuyla alınabilir.
Çalışma zamanında otomatik Deadlock tespiti, Watchdog zamanlayıcıları aracılığıyla uygulanır. Bir iş parçacığı belirtilen bir zaman aşımı içinde bir işlemi tamamlamazsa, watchdog bir döküm başlatır ve Crash Reporting sistemine (Firebase Crashlytics, Sentry) bir rapor gönderir. Sentry'ye (Issue Resolution Report, 2024) göre, bir watchdog yapılandırmak Deadlock teşhis süresini haftalardan birkaç saate düşürür.
Geliştirme aşamasında, JetBrains ThreadSafe statik analizörü ve Lock Checker modülüyle Checker Framework etkilidir. Bu araçlar, kaynak kodu düzeyinde kilit edinme sırasını analiz eder ve olası döngüler hakkında uyarır. Ek olarak, Test-Driven Deadlock Detection önerilir — yüzlerce iş parçacığında farklı kilit sıralarıyla işlemler çalıştıran stres testleri.
Özel dikkat Cooperative Deadlock Detection'ı hak ediyor — iş parçacıklarının küresel bir kayıt defteri aracılığıyla edinilen kilitler hakkında bilgi alışverişinde bulunduğu bir yöntem. Bir iş parçacığı olası bir döngü tespit ederse, tüm kaynakları serbest bırakır ve işlemi yeniden dener. Bu yaklaşım dağıtık sistemlerde (Apache ZooKeeper, Google Chubby) kullanılır ve Jetpack Sync gibi kütüphaneler aracılığıyla yavaş yavaş mobil geliştirmeye dahil edilmektedir.
En güvenilir yol, uygulama genelinde küresel bir kilit edinme sırası oluşturmaktır. Tüm iş parçacıkları her zaman önce daha küçük numaralı kilidi, ardından daha büyük numaralı kilidi edinirse, döngüsel bekleme (Circular Wait koşulu) imkansızdır. Büyük projelerde sıra belgelenir ve kod incelemesi yoluyla doğrulanır.
TryLock, bir iş parçacığını süresiz olarak bloke etmeyen, ancak belirtilen bir süre içinde kilit edinilmezse false döndüren bir kilitleme yöntemidir. Java'da bu, ReentrantLock.tryLock(timeout, TimeUnit) aracılığıyla, Kotlin Coroutines'de — zaman aşımı ile Mutex.withLock aracılığıyla uygulanır. Başarısızlık durumunda, iş parçacığı edinilen tüm kaynakları serbest bırakır ve daha sonra yeniden dener.
Banker Algoritması, Edsger Dijkstra tarafından önerilen teorik bir Deadlock önleme yöntemidir. Kaynak tahsisini banka işlemleri olarak modeller: sistem, güvenli olmayan bir duruma (deadlock) yol açabilecekse bir kaynağı tahsis etmez. Pratikte, iş parçacıklarının maksimum ihtiyaçlarını önceden bilmenin zorluğu nedeniyle mobil geliştirmede nadiren kullanılır, ancak ilkeleri SQLite veritabanlarında ve dosya sistemlerinde kullanılır.
Sıkça Sorulan Sorular
Hayır, karşılıklı kilitleme için en az iki iş parçacığı gerekir. Tek iş parçacıklı kodda, tüm işlemler sırayla yürütülür, bu nedenle döngüsel bekleme imkansızdır. Ancak, dosya kilitleri veya işlemler arası semaforlar kullanılırken süreçler arasında Deadlock oluşabilir.
Coroutine'lerde Deadlock, askıya alma işlevleri (suspend) düzeyinde oluşur ve OS iş parçacığını bloke etmez, bu da onu daha az fark edilir kılar. kotlinx.coroutines'deki Mutex askıya alan (suspending) bir kilittir — iş parçacığını bloke etmez, ancak coroutine yürütülmez. Tespit için kotlinx-coroutines-debug modülünden DebugProbes kullanın.
SQLite Deadlock, iki veritabanı bağlantısının işlemleri farklı sıralarda yürütmeye çalışmasıyla oluşur. SQLite bu tür durumları tespit eder ve SQLITE_BUSY veya SQLITE_LOCKED hata kodunu döndürür. Android'de, tek bir veritabanı örneği ve @Transaction aracılığıyla işlemlerle Room kullanılması önerilir, bu da bağlantılar arası Deadlock'u ortadan kaldırır.
Android Runtime, bir ANR (Application Not Responding) oluşturulduğunda çalışan yerleşik bir Deadlock dedektörüne sahiptir. Sistem, uygulamanın tüm iş parçacıklarının Thread Dump'ını analiz eder ve karşılıklı kilitlemeleri işaretler. Sonuç /data/anr/traces.txt adresinde ve Google Play Console'un ANR Reports bölümünde mevcuttur.
İlk olarak, uygulamanın tüm iş parçacıklarının Thread Dump'ını alın. Her iş parçacığının hangi kilitleri tuttuğunu ve hangilerini edinmeye çalıştığını analiz edin. Zaman sınırı aşıldığında otomatik döküm yapan bir Watchdog zamanlayıcısı uygulayın. Düzeltmeden sonra, tekrarlanmayı önlemek için CI hattınıza ThreadSafety lint kuralını ekleyin.
Özet
Anahtar teslim bir mobil uygulama geliştireceğiz
IT Sectr, 2017'den beri girişimler ve işletmeler için iOS ve Android uygulamaları oluşturmaktadır. Size danışmanlık yapacak ve en iyi çözümü önereceğiz.
Ayrıca okuyun