Bankamatik sorunu
Videodaki örnek bir para çekme fonksiyonu: Bakiye yeterliyse bakiyeden düş, sonra parayı ver. Tek başına çalışınca kusursuz. Ama aynı hesaptan iki kişi aynı anda son 10 lirayı çekmeye kalkarsa, iki işlem de “bakiye yeterli” kontrolünü geçebilir. İkisi de parayı alır, bakiye eksiye düşer.
Buna race condition (yarış durumu) deniyor: Sonuç, hangi işin hangi anda araya girdiğine bağlı. Kontrol ile yazma arasındaki o hassas bölgeye kritik bölge (critical section) deniyor. İstediğimiz şey basit: Bu bölgeye aynı anda sadece bir kişi girsin. Tuvalet kapısının kilidi gibi. Kilitliyse beklersin, çıkan kilidi açar.
Neden basit bir bayrak yetmez?
İlk akla gelen şu: Bir kilitli değişkeni tut, false ise true yap ve içeri gir. Ama bu, çözmeye çalıştığın sorunun ta kendisi. İki thread aynı anda false okur, ikisi de true yazar, ikisi de kilidi aldığını sanır. Okuma ve yazma iki ayrı adım olduğu sürece araya biri girebilir.
Donanımdan gelen tek hamle
Çözüm donanımdan geliyor: Okumayı ve yazmayı bölünmez (atomik) tek bir işlemde yapan komutlar. x86’daki XCHG, bellekteki bir değere yeni bir değer yazıp eskisini geri veriyor. Intel’in belgesine göre bellekle kullanıldığında kilitleme kendiliğinden devreye giriyor, kimse araya giremiyor.
#include <stdatomic.h>
void lock(atomic_int *m) {
while (atomic_exchange(m, 1) == 1) { } // 1 geldiyse biri içeride, tekrar dene
}
void unlock(atomic_int *m) {
atomic_store(m, 0);
}
İçeri 1 yazıp eskiden 0 aldıysan kilit artık senin. 1 aldıysan zaten doluymuş, hiçbir şeyi bozmadın ve tekrar deniyorsun. Benzer bir yapı taşı da compare-and-swap: “Değer hâlâ beklediğim gibiyse yenisini yaz.”
Dönmek mi, uyumak mı?
Bu döngü kapıyı saniyede milyarlarca kez yoklamak demek. İşlemci çekirdeği yüzde 100 çalışıyor ama hiçbir iş yapmıyor. Buna spin lock deniyor. Godbolt’a göre gecikmenin çok kritik olduğu işlerde bu bilinçli bir tercih olabilir. Normal programlarda ise işletim sisteminin kilidi birkaç denemeden sonra thread’i uyutuyor, kilit açılınca uyandırıyor. Linux’ta bunun temeli futex: Kilit boşken her şey kullanıcı alanında atomik komutlarla bitiyor, çekirdeğe sadece beklemek gerektiğinde gidiliyor.
Sıralama tuzağı
Derleyici de işlemci de, birbirinden bağımsız görünen komutların yerini değiştirebiliyor. Kilit almadan önce bakiyeyi okumak felaket olur. Bu yüzden kilit almak bir acquire, bırakmak bir release bariyeri gibi davranmalı. Videoya göre x86 bu konuda epey hoşgörülü, modern ARM işlemciler ise çok daha fazla yeniden sıralama yapıyor. Kendi kilidini yazmak yerine dilin hazır mutex’ini kullanman için iyi bir sebep.
Deadlock
Hesap başına bir kilit olsun. A’dan B’ye para aktarırken iki kilidi de alıyorsun. Aynı anda biri B’den A’ya aktarıyorsa, sen A’yı o B’yi tutar ve ikiniz de sonsuza kadar bekler. Bu bir deadlock. Yaygın çözüm: Kilitleri hep aynı sırayla al, örneğin hesap numarasına göre küçükten büyüğe. Videoya göre veritabanları ise zaman aşımıyla işlemi geri alıp yeniden deneyebiliyor.
Pratikte
Rust’ta Mutex<T> veriyi içinde tutuyor. Veriye sadece kilit alarak ulaşabiliyorsun, kilit de kapsam bitince kendiliğinden açılıyor. Unutmak zorlaşıyor. Go’da sync.Mutex daha çıplak: Lock ve Unlock çağrılarını sen eşleştiriyorsun. Thread’lerin bellek paylaşımı için işletim sistemi kavramları yazısına, eşzamanlı tasarımları adım adım denetlemek için TLA+ yazısına bakabilirsin.
Tek thread’li async kodda da benzer sorun var: “Kontrol et, sonra yaz” arasına bir await girerse, o arada başka bir iş aynı veriyi değiştirebilir.
Düşün
- Kodunda “önce kontrol et, sonra yaz” yapan bir yer var mı? Aynı anda iki istek gelse ne olurdu?
- Herkesin aynı kilidi beklediği bir sistemde, kilit yerine veriyi nasıl bölerdin?
Videoyu izle
Kaynaklar (8, İngilizce)
- Computerphile: Mutex Explained (video) (yeni sekmede açılır)
- x86 instruction reference (Intel SDM): XCHG (yeni sekmede açılır)
- Linux man-pages: futex(2) (yeni sekmede açılır)
- Rust std: std::sync::Mutex (yeni sekmede açılır)
- Rust std: std::sync::atomic (module) (yeni sekmede açılır)
- Rust std: atomic::Ordering (Acquire, Release) (yeni sekmede açılır)
- Go: sync.Mutex (yeni sekmede açılır)
- Wikipedia: Dining philosophers problem (resource hierarchy solution) (yeni sekmede açılır)
Kendi cümlelerimizle yazılmış bir özettir, çeviri değildir. Tüm kaynaklar yukarıda.