Semaphore, bir sayaç ve bekleyen iş parçacıkları kuyruğu aracılığıyla paylaşılan bir kaynağa erişimi kontrol eden bir senkronizasyon ilkelidir. Wikipedia, 2024'e göre, semafor 1965 yılında Edsger Dijkstra tarafından çoklu iş parçacığı etkileşimi sorunlarını çözmek için önerilmiştir. Araç, kritik bir bölümle aynı anda çalışan iş parçacığı sayısını sınırlamaya olanak tanır.
Ana Noktalar
Semaphore, paylaşılan bir kaynağa erişimi kontrol etmek için bir sayaç kullanan bir senkronizasyon ilkelidir. Bu kavram, 1965 yılında Edsger Dijkstra tarafından önerilmiş ve işletim sistemlerindeki tüm modern senkronizasyon mekanizmalarının temeli olmuştur.
Semafor, iki atomik işleme sahip bir tamsayı değişkendir: wait (acquire) ve signal (release). Wait işlemi sayacı azaltırken, signal işlemi sayacı artırır. Sayaç sıfıra ulaştığında, wait'i çağıran iş parçacığı başka bir iş parçacığı signal yürütünceye kadar bloke edilir.
Bir semaforun temel amacı, kritik bölümleri birden çok iş parçacığının aynı anda erişiminden korumaktır. Mutex'in aksine, semafor bir sahip iş parçacığına bağlanma gerektirmez, bu da onu daha geniş bir koordinasyon görevi yelpazesi için uygun hale getirir.
Semafor kavramı, Technische Hogeschool Eindhoven'da geliştirilen THE işletim sistemi bağlamında ortaya çıkmıştır. Dijkstra, semaforu matematiksel bir soyutlama olarak biçimlendirmiş ve herhangi bir senkronizasyon ilkelini uygulamak için yeterliliğini kanıtlamıştır.
Semafor mekanizması, iki atomik işlem ve bir iç bekleme kuyruğuna dayanır. Acquire çağrıldığında, iş parçacığı sayaç değerini kontrol eder ve ya yürütmeye devam eder ya da kaynak serbest bırakılana kadar bloke olur.
Bir semafor oluştururken, izin sayacının bir başlangıç değeri belirlenir. Her acquire çağrısı sayacı 1 azaltır. Bundan sonra sayaç negatif olursa, iş parçacığı bloke edilir. Release işlemi sayacı artırır ve bekleyen iş parçacıklarından birini uyandırır.
import java.util.concurrent.Semaphore
val semaphore = Semaphore(3)
fun accessResource() {
semaphore.acquire()
try {
println("${Thread.currentThread().name} çalışıyor")
} finally {
semaphore.release()
}
}
Bir iş parçacığı sıfır sayaçla acquire çağırdığında, işletim sistemi onu semaforun FIFO kuyruğuna yerleştirir. İş parçacığı BLOCKED durumuna geçer ve CPU zamanı tüketmez. Bir release çağrısından sonra, kuyruktaki ilk iş parçacığı RUNNABLE durumuna geçer ve kaynağa erişim sağlar.
Senkronizasyon teorisinde, iki ana semafor türü ayırt edilir: ikili ve sayısal. Türün seçimi, kaynaklara erişim yönetiminin belirli görevine bağlıdır.
İkili semafor yalnızca 0 ve 1 değerlerini alır. Davranış olarak mutex'e benzer, ancak sahiplik gereksinimi olmadan — herhangi bir iş parçacığı release yürütebilir. Bu tür semaforlar, iş parçacıkları arasında hazır olma bayrakları ve olayları uygulamak için uygundur.
val ready = Semaphore(0)
fun producer() {
Thread.sleep(1000)
ready.release()
}
fun consumer() {
ready.acquire()
println("Veriler hazır")
}
Sayısal semafor negatif olmayan herhangi bir değeri alabilir. Birden çok örneğin bulunduğu benzer kaynaklardan oluşan bir havuzu yönetmek için kullanılır. Örneğin, 5 ağ bağlantısından oluşan bir havuz: her acquire bir bağlantı alır, release onu havuza geri verir.
Sayısal semaforlar, harici hizmetlere erişim hızını sınırlamak ve iş parçacığı havuzları uygulamak için vazgeçilmezdir. Manuel iş parçacığı yönetimi olmadan paralellik derecesini hassas bir şekilde kontrol etmeye olanak tanırlar.
| Parametre | İkili Semafor | Sayısal Semafor |
|---|---|---|
| Aralık | 0 veya 1 | 0'dan N'ye |
| Eşzamanlı iş parçacığı | 1 | N'e kadar |
| Uygulama | sinyalleşme, bayraklar | kaynak havuzları, hız sınırlama |
Geliştiriciler genellikle semafor ve mutex'i karıştırır, ancak aralarında temel farklılıklar vardır. Bu farklılıkları anlamak, bir projede doğru senkronizasyon mekanizmasını seçmek için kritik öneme sahiptir.
Temel fark sahiplik kavramıdır. Bir mutex her zaman hangi iş parçacığının onu aldığını bilir ve yalnızca o iş parçacığı onu serbest bırakabilir. Bir semaforun sahibi yoktur: herhangi bir iş parçacığı acquire çağırmadan bile release çağırabilir. Bu, mutex'i veri koruması için daha güvenli ve semaforu koordinasyon için daha esnek hale getirir.
Pratikte, mutex tipik senaryolar için optimizasyonlar sayesinde basit karşılıklı dışlamada daha hızlıdır. Semafor, sayacı korumak için ek yük gerektirir. Ancak, paralelliği sınırlamak veya üretici-tüketici modelini uygulamak için semafor vazgeçilmezdir.
| Özellik | Semaphore | Mutex |
|---|---|---|
| Sahiplik | sahip yok | sahibi var |
| Serbest bırakma | herhangi bir iş parçacığı | yalnızca sahip iş parçacığı |
| Sayaç | 0'dan N'ye | ikili |
| Kullanım durumu | paralellik sınırlama ve sinyalleşme | kritik bölüm koruması |
| Yineleme | hayır | evet (yeniden girebilir) |
Mobil uygulama geliştirmede, Semaphore sınırlı kaynaklara (ağ bağlantıları, dosyalar, veritabanları ve donanım bileşenleri) erişimi yönetmek için kullanılır. Modern platformlar kullanışlı yerleşik uygulamalar sağlar.
Tipik bir kullanım durumu HTTP bağlantı havuzudur. Bir uygulama, sağlayıcının API'si paralelliği sınırladığı için bir sunucuya aynı anda en fazla 4 istek gönderebilir. Başlangıç değeri 4 olan bir semafor, herhangi bir yük altında eşzamanlı istek sayısının limiti aşmamasını sağlarken, diğer iş parçacıkları kuyrukta bekler.
Semafor olmadan, kullanıcı etkinliğindeki keskin bir artış sunucu altyapısında ani aşırı yüklenmeye neden olabilir, bu da zaman aşımlarına ve 429 Too Many Requests hatalarına yol açar. Semaphore bir sigorta gibi çalışır, aktif iş parçacığı sayısından bağımsız olarak kesin olarak belirtilen sayıda eşzamanlı çağrıya izin verir.
Android, java.util.concurrent paketinden Semaphore sınıfını sağlar. Sunucu aşırı yüklenmesini önlemek için eşzamanlı ağ isteklerini iki iş parçacığıyla sınırlama örneğine bakalım.
class ApiClient {
private val throttle = Semaphore(2)
suspend fun fetch(url: String): Result {
throttle.acquire()
return try {
httpGet(url)
} finally {
throttle.release()
}
}
}
iOS'te, GCD'den DispatchSemaphore aynı görevi çözer. Geliştiriciler, ana iş parçacığını bloke etmeden eşzamansız kodda kaynaklara erişimi senkronize etmek için kullanır.
let semaphore = DispatchSemaphore(value: 3)
func processBatch(_ items: [UIImage]) {
for img in items {
semaphore.wait()
DispatchQueue.global().async {
applyFilter(to: img)
semaphore.signal()
}
}
}
En yaygın hata, bir istisna oluştuğunda unutulan release'dir. Bir iş parçacığı release çağırmadan önce bir hatayla sonlanırsa, semafor diğer iş parçacıkları için kalıcı olarak bloke kalır. Garantili serbest bırakma için try/finally veya defer kullanın. İkinci sorun, farklı iş parçacıkları tarafından farklı sırada birden çok semafor alırken deadlock'tur.
Semaforlar yalnızca veri koruması için değil, aynı zamanda karmaşık çok iş parçacıklı senaryolarda iş parçacığı koordinasyonu için de kullanılır. Yaygın modelleri bilmek geliştirmeyi hızlandırır ve senkronizasyon hataları olasılığını azaltır.
Gerçek projelerde semafor kullanımının birkaç kanıtlanmış modeli vardır. Bunları bilmek, tipik hatalardan kaçınmaya ve güvenilir çok iş parçacıklı sistemler oluşturmaya yardımcı olur.
Başlangıç değeri N ve bir zamanlayıcı aracılığıyla periyodik release ile bir semafor, API isteklerinin hız sınırlamasını uygular. Örneğin, bir hizmet saniyede 10 isteğe izin verir: semafor 10'da başlar, her istek sayacı azaltır ve ayrı bir TimerTask her saniye sayacı başlangıç değerine döndürür. Bu, hem uygulamayı hem de sunucuyu aşırı yüklenmeden korur.
Klasik üretici-tüketici probleminde, iki semafor bir arabelleği yönetir: empty (yazma izinleri) ve full (okuma izinleri). Üretici empty'de acquire ve full'de release çağırırken, tüketici tersini yapar. Bu şema, tüketicinin asla boş bir arabellek okumamasını ve üreticinin asla onu taşırmamasını garanti eder.
Aynı şema, işletim sistemlerindeki sınırlı arabelleğin — sabit boyutlu bir halka arabelleğin temelini oluşturur. Mobil uygulamalarda, model görüntü, video dosyası ve analitik olay kuyruklarını işlemek için kullanılır.
Semaforlar, arka plan hizmetlerinde ağ çağrılarını sınırlamak için başarıyla kullanılır. Örneğin, bir analitik uygulaması sunucuya olay paketleri gönderir. Yoğun yük zamanlarında (uygulama başlatma, çevrimdışı sonrası senkronizasyon) eşzamanlı iş parçacıklarını sınırlamadan, eşzamanlı istek sayısı sunucu limitlerini aşabilir. Başlangıç değeri 3 olan bir semafor, sorunsuz gönderme sağlar ve sunucu tarafında blokajı önler.
Sıkça Sorulan Sorular
Semaphore yalnızca bir sayaç değil, atomik işlemleri ve bekleme kuyruğu olan bir senkronizasyon ilkelidir. Normal bir sayaç, iş parçacığını bloke etmez ve birden çok iş parçacığı eşzamanlı eriştiğinde atomik artışı garanti etmez.
Evet, farklı iş parçacıkları tarafından farklı sırada birden çok semafor alındığında deadlock mümkündür. Örneğin, iş parçacığı A önce S1'i, sonra S2'yi alırken, iş parçacığı B önce S2'yi, sonra S1'i alır. Projedeki tüm semaforlar için tek bir alma sırası belirleyin.
İş parçacığı bloke olur ve bekleme durumuna girer. Başka bir iş parçacığı release çağırana kadar CPU zamanı tüketmez. Java'da bu BLOCKED durumudur; Swift'te iş parçacığı GCD tarafından askıya alınır.
Temel fark sahipliktir. Bir Mutex yalnızca sahip iş parçacığı tarafından serbest bırakılabilir. Bir Binary Semaphore herhangi bir iş parçacığı tarafından serbest bırakılabilir, bu iş parçacıkları arasında sinyalleşme için uygundur ancak veri bütünlüğünü korumak için daha az güvenlidir.
Başlangıç değeri senaryoya bağlıdır. Tek bir kaynağı korumak için — 1. N bağlantılı bir havuz için — N. İş parçacıkları arasında sinyalleşme için, tüketici iş parçacığının üreticiden sinyal beklemesi için 0 kullanın.
Ö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