Race Condition, çoklu iş parçacığı programlamada nihai sonucun iş parçacıklarının yürütülme sırasına bağlı olduğu bir durumdur. Oracle Java Tutorials (2024)’e göre, yarış durumu, birden fazla iş parçacığının senkronizasyon olmadan ortak bir kaynağa erişmesiyle oluşur. Uygun mekanizmalar olmadan, Race Condition veri bozulmasına ve mobil uygulamalarda tekrarlanamayan hatalara yol açar.
Önemli Noktalar
Race Condition, çoklu iş parçacığı programında çalışmanın doğruluğunun iş parçacıklarının öngörülemeyen yürütme sırasına bağlı olduğu bir hatadır. İki veya daha fazla iş parçacığı senkronizasyon olmadan aynı anda ortak bir kaynağa eriştiğinde, kaynağın nihai durumu tanımsız hale gelir.
Mobil geliştirmede Race Condition özellikle tehlikelidir çünkü iş parçacıkları farklı CPU çekirdeklerinde farklı hızlarda çalışabilir. Geliştirici hangi iş parçacığının işlemi önce tamamlayacağını kontrol edemez — buna işletim sistemi zamanlayıcısı karar verir. IBM araştırmasına (Concurrency Bugs in Android, 2022) göre, Android uygulamalarındaki kritik hataların yaklaşık %23’ü yarış durumlarıyla ilgilidir.
Race Condition’un temel özelliği belirlenemezliğidir. Aynı kod binlerce kez hatasız çalışabilir ve sonra aniden çökebilir. Bu da teşhisi özellikle zorlaştırır: hata yalnızca belirli koşulların birleşimi altında ortaya çıkar — CPU yükü, aktif iş parçacığı sayısı ve zamanlama aşaması.
Race Condition, bir iş parçacığı atomik olmayan bir işlem gerçekleştirdiğinde oluşur — başka bir iş parçacığı tarafından kesintiye uğratılabilecek birden fazla adımdan oluşan bir dizi. Örneğin, counter++ artırma işlemi aslında üç adımdan oluşur: değeri bellekten okuma, bir artırma ve geri yazma. İki iş parçacığı bu adımları iç içe geçirirse, sonuç hatalı olacaktır.
Yarış durumunun ana nedeni, ortak verilere erişirken senkronizasyon eksikliğidir. Bir iş parçacığı bir nesneyi değiştirirken başka bir iş parçacığı aynı anda onu okuduğunda, okuma sonucu tahmin edilemez. Android’de bu sorun, uygulama bileşenlerinin (Activity, Service, BroadcastReceiver) farklı iş parçacıklarında çalışabilmesi nedeniyle daha da karmaşık hale gelir.
Modern Kotlin Android geliştirmede Race Condition genellikle coroutine’lerin yanlış kullanımı nedeniyle oluşur. İki coroutine senkronizasyon olmadan farklı Dispatcher’larda ortak durumla çalışırsa, sonuç tahmin edilemez olur. Bu, özellikle Dispatchers.IO ve Dispatchers.Main’i ortak değişken nesnelerle birleştirirken yaygındır.
Veri yarışının klasik bir örneğini ele alalım — birden çok iş parçacığından sayaç artırma. Senkronizasyon olmadan, işlemler üst üste bindiği için nihai değer beklenenden düşük olacaktır.
class RaceCounter {
private var counter = 0
fun increment() {
// Atomik olmayan işlem — üç adım
counter++ // okur, artırır, yazar
}
fun getCount(): Int = counter
}
fun main() = runBlocking {
val rc = RaceCounter()
val jobs = List(1000) {
launch(Dispatchers.Default) {
rc.increment()
}
}
jobs.forEach { it.join() }
println(rc.getCount()) // 1000 bekleniyor, ~997 alındı
}
Bu örnekte, 1000 coroutine aynı anda increment() çağırır. counter++ işleminin atomik olmayan doğası nedeniyle, nihai değer neredeyse hiçbir zaman 1000’e eşit olmaz. Her çalıştırma farklı bir sonuç üretir — Race Condition’un klasik bir belirtisi. Yarışa ne kadar çok iş parçacığı katılırsa, beklenen değerden sapma o kadar büyük olur.
Çözüm, atomik bir tür veya kilit kullanmaktır. Kotlin’de java.util.concurrent.atomic paketinden AtomicInteger bu görev için uygundur. Okuma-değiştirme-yazma işlemlerinin CPU düzeyinde tek bir bölünemez eylem olarak yürütülmesini garanti eder.
import java.util.concurrent.atomic.AtomicInteger
class SafeCounter {
private val counter = AtomicInteger(0)
fun increment() {
counter.incrementAndGet() // atomik işlem
}
fun getCount(): Int = counter.get()
}
Data Race en yaygın Race Condition türüdür. Bir iş parçacığı bir değişkene veri yazarken başka bir iş parçacığı aynı anda aynı değişkeni senkronizasyon olmadan okur veya yazarsa oluşur. Java Bellek Modeli’nde bu davranış tanımsız kabul edilir — bir iş parçacığı CPU düzeyinde önbellekleme nedeniyle güncel olmayan bir değer görebilir.
Check-Then-Act deseni, bir iş parçacığının bir koşulu kontrol ettiği ve ardından bu kontrole dayalı bir eylem gerçekleştirdiği bir durumdur. Kontrol ve eylem arasında başka bir iş parçacığı durumu değiştirebilir. Tipik bir örnek: bir öğenin koleksiyonda olup olmadığını kontrol etme ve ardından kaldırma. Android’de bu, SharedPreferences veya veritabanlarıyla çalışırken yaygındır.
Read-Modify-Write, bir iş parçacığının bir değeri okuduğu, yerel bellekte değiştirdiği ve geri yazdığı bir durumdur. Okuma ve yazma arasında başka bir iş parçacığı orijinal değeri değiştirdiyse, değişiklik sonucu kaybolur. Klasik örnek, yukarıda Kotlin kodunda incelenen counter++ işlemidir.
Software Transactional Memory (STM), ortak veriler üzerindeki işlemlerin veritabanlarına benzer şekilde işlemler halinde gerçekleştirildiği bir yaklaşımdır. İki işlem çakışırsa, biri geri alınır ve yeniden denenir. JVM için Kotlin’de, açık kilitler olmadan erişim çakışmalarını otomatik olarak işleyen Multiverse STM kütüphanesi mevcuttur. STM, özellikle birden çok birbirine bağlı nesneyle çalışırken Android’de kullanışlıdır.
Race Condition’un özel bir kategorisi, Activity yaşam döngüsüyle ilgili ince yarışlardır (thin races). Tipik bir senaryo: bir arka plan iş parçacığı veri yüklemeyi bitirir, ancak Activity zaten yok edilmiştir (ekran dönüşü). Coroutine var olmayan bir View’ı güncellemeye çalışır ve IllegalStateException ile çöker. Çözüm, Lifecycle Owner yok edildiğinde coroutine’leri otomatik olarak iptal eden viewModelScope ve Lifecycle-aware bileşenlerini kullanmaktır.
Race Condition’u tespit etmek, çoklu iş parçacıklı uygulamaları hata ayıklamadaki en zor görevlerden biridir. Standart testler, yarış durumlarını nadiren ortaya çıkarır çünkü bunlar yalnızca belirli zamanlama tesadüfleri altında kendini gösterir. Google’a (Android Testing Guide, 2023) göre, Race Condition’ların yaklaşık %70’i, test ortamındaki belirleyici yürütme sırası nedeniyle birim testleri tarafından tespit edilmez.
Ana tespit yöntemleri özel araçları içerir. ThreadSanitizer (TSan), Android NDK’ya yerleşik, tüm bellek erişimlerini izleyen ve senkronize olmayan erişimleri tespit eden dinamik bir analizördür. Java/Kotlin kodu için Google, arka plan iş parçacıklarından UI iş parçacığına yasa dışı erişimleri kesen StrictMode ile birlikte Android Studio Layout Inspector’ı önerir.
Bir diğer etkili yaklaşım, yük altında tekrarlanan test çalıştırmalarıyla Stres Testi yapmaktır. JetBrains’in Lincheck framework’ü, JVM üzerinde eşzamanlı veri yapılarını test etmek için özel olarak tasarlanmıştır. Farklı işlem permütasyonlarıyla senaryoları otomatik olarak oluşturur ve her durumda sonuçların doğruluğunu doğrular.
| Araç | Platform | Analiz Türü |
|---|---|---|
| ThreadSanitizer | Android NDK | Dinamik bellek analizi |
| Intel Inspector | Windows | Statik + dinamik |
| Lincheck | JVM / Kotlin | Stres testi |
| StrictMode | Android | Çalışma zamanı kesimi |
Atomik değişkenler (AtomicInteger, AtomicLong, AtomicReference), tek işlemler için veri yarışlarını ortadan kaldırmanın en kolay yoludur. Kilit olmadan atomik olarak yürütülen düşük seviyeli CPU CAS talimatlarını (Compare-And-Swap) kullanırlar. Bu, düşük çekişmeli senaryolarda maksimum performans sağlar.
Mutex ve kilitler, karmaşık işlemler ve kritik bölümler için uygun klasik bir senkronizasyon mekanizmasıdır. Kotlin’de coroutine’ler için, iş parçacığını bloke etmek yerine askıya almayı destekleyen kotlinx.coroutines kütüphanesinden askıya alınabilir Mutex kullanılır. Bu, geleneksel kilitlerin karakteristik boşta beklemesini önler.
Durum yalıtımı, her iş parçacığının kendi veri kopyasıyla çalıştığı bir mimari yaklaşımdır. Mobil geliştirmede bu, her aktörün kendi durumuna sahip olduğu ve diğer aktörlerle mesajlar aracılığıyla iletişim kurduğu Aktör modeliyle elde edilir. Kotlin Coroutines, Channel ve SendChannel aracılığıyla Aktör uygulaması sağlar ve bu da Race Condition’u mimari düzeyde tamamen ortadan kaldırır.
Ek bir koruma katmanı Değişmezliktir: paylaşılan veriler temelde değişmezse, Race Condition senkronizasyon olmadan bile imkansız hale gelir. Kotlin’de bunun için val alanlarına sahip data class’lar ve iş parçacıkları arasında yayınlarken yapısal değişmezliği garanti eden kotlinx.collections.immutable koleksiyonları kullanılır.
Sıkça Sorulan Sorular
Data Race, iki iş parçacığının aynı belleğe aynı anda eriştiği ve en az birinin yazma yaptığı belirli bir Race Condition türüdür. Race Condition, mantıksal yarış durumları da dahil olmak üzere iş parçacığı yürütme sırasına bağlı tüm hataları içeren daha geniş bir kavramdır.
Tamamen ortadan kaldırılamaz ancak en aza indirilebilir. Değişmez nesneler, atomik türler ve tek iş parçacıklı dağıtıcılı coroutine’ler kullanın. ThreadSafety kuralıyla Android Lint gibi statik analiz araçları, derleme zamanında potansiyel yarışları belirlemeye yardımcı olur.
UI uygulamalarında Race Condition genellikle ekran titremesi, yanlış veri görüntüleme veya liste güncellemesi sırasında çökme olarak ortaya çıkar. Tipik bir senaryo: bir arka plan iş parçacığı veri yükler ve adaptörü güncellerken kullanıcı listeyi kaydırır — Adapter DataSet’ine aynı anda erişim oluşur.
volatile, iş parçacıkları arasında değişikliklerin görünürlüğünü garanti eder — volatile bir değişkenine yazma, tüm iş parçacıkları tarafından hemen görülür. Ancak volatile, bileşik işlemler için atomiklik sağlamadığından Read-Modify-Write ve Check-Then-Act sorunlarını çözmez. Bu tür senaryolar kilitler veya atomik sınıflar gerektirir.
Kotlin Coroutines’de Race Condition, OS iş parçacığı zamanlayıcısında değil, coroutine zamanlayıcısı seviyesinde oluşur. Coroutine’ler askıya alma noktalarında (suspend) geçiş yapabilir, bu da yarışlar için ek fırsatlar yaratır. kotlinx.coroutines.debug aracı ve IntelliJ IDEA hata ayıklayıcısı, coroutine durumunu izlemeye yardımcı olur.
Ö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