Ders 11 / 14
Dışlama Kilitleri
Bir goroutine kilidi tutan başka bir goroutine kilidi bırakana kadar bloklanıyor ve durma sınıfı beklediği geldi oluyor. Aynı sayaç kilitli ve kilitsiz iki kurulumla 20 goroutine ve goroutine başına 100 artışla işletiliyor, ikisi de 2000 veriyor — çünkü kilitsiz kurulum gerçek bir yarış kurmuyor, her goroutine kendi gözesine yazıyor. Sınırlayıcı ölçüm, kilit kapsamı dışına taşan tek bir yazmayı kanalla zorlanmış bir sırayla modelliyor: iki artış uygulanıyor ama sonuç 101 yerine 1 çıkıyor.
İçindekiler
Önceki dersin kapanışı bir sınır çizdi: zaman aşımı bekleyen goroutine’i kurtarıyor ama beklenen işin kendisini durdurmuyor — iş goroutine’i hâlâ koşuyor, yalnız kimse onu beklemiyor. Bu ders aynı sorudan uzaklaşıyor. Şimdiye kadarki her ders goroutine’lerin birbirine bir değer aktarmasını ölçtü: kanaldan geçen bir sayı, bir bağlamın iptal sinyali, bir zaman aşımının seçtiği dal. Bu derste goroutine’ler birbirine bir şey göndermiyor; aynı değeri paylaşıyor. İletişim yerine ortak bir durumu korumayı seçmek ne değiştiriyor?
Kanal tabanlı düzenekler bu sorudan hep kaçındı: bir kanaldan geçen değer kopyalanıyordu, gönderen taraf değeri gönderdikten sonra ona dokunmuyordu, dolayısıyla iki goroutine’in aynı belleğe aynı anda dokunması gibi bir durum hiç kurulmuyordu. Paylaşılan durum bu kaçışı kapatıyor: birden çok goroutine gerçekten aynı değişkeni okuyor ve yazıyor. Kursun ikinci sert kısıtı burada devreye giriyor — paylaşımın korunmadığı hâlini koşturup yanlış bir sonuç üretmek bu derste yapılmıyor; onun yerine korumanın ne sağladığı ve nerede bittiği ölçülüyor.
Kilidin Kapsamı
Karşılıklı dışlama kavramı — aynı anda yalnız bir yürütme biriminin bir kaynağa erişmesini
zorlamak — İşletim Sistemi Kavramları kursunda kuruldu ve orada serpiştirme sayılarak
ölçüldü; burada tekrarlanmıyor. Bu derste ölçülen şey kavramın kendisi değil, Go’nun onu
gerçekleştiren tek aracı: sync.Mutex, bir dışlama kilidi. Kilidin sunduğu sözleşme
kısa: Lock çağrısı kilidi tutan biri varsa döner ama önce bloklanır, Unlock kilidi
serbest bırakır. Lock ile Unlock arasında kalan satırlar kilidin kapsamıdır — kilit
yalnız bu satırların aynı anda tek bir goroutine tarafından çalıştırılmasını garanti eder,
kapsam dışında kalan hiçbir şeye karışmaz. Bu kapsam, İşletim Sistemi Kavramları kursunun
kritik bölge dediği şeyin Go’daki karşılığı: kritik bölge kavramı orada nasıl
sınırlandığından bağımsız kuruldu, sync.Mutex burada onu iki çağrı arasındaki bir aralık
olarak somutlaştırıyor.
sync.Mutexin bir bildirim dışında hiçbir kurulum gerektirmemesi de sözleşmenin bir
parçası: var kilit sync.Mutex yazıldığı anda kilit kullanıma hazır, çünkü kilidin sıfır
değeri zaten kilitlenmemiş bir kilit. Go Temelleri kursunun sıfır değer bahsine burada tek
cümlelik bir atıf yeterli — kilidin kendisi bu kursun ölçü ekseni değil, yalnız bir araç.
Artir yöntemindeki defer s.mu.Unlock() yazımı da bu sözleşmenin bir parçası: kilidi
bırakan çağrı, yöntemin gövdesi nereden dönerse dönsün — normal dönüş, erken dönüş, ya da
bir panikle — çalışır. Kilidi elle, gövdenin sonuna yazan bir Unlock() çağrısıyla
bırakmak, gövde ortasında bir erken dönüş eklendiğinde kilidin hiç bırakılmadan kalmasına
açık kapı bırakır; defer bu boşluğu kapatıyor. Bu ders bu boşluğu ayrıca koşturmuyor,
çünkü konu bu dersin sınırlayıcı ölçümünde zaten farklı bir biçimde — kilit kapsamının
kendisinin yanlış çizilmesi olarak — ele alınıyor.
Bu sözleşmenin ilk sonucu, kursun ilk dersinden beri sorulan soruya doğrudan bağlanıyor:
kilidi bekleyen bir goroutine nasıl duracak? Yanıt ortak tanımın kendi satırlarından biri
— kilidi tutan goroutine Unlock çağırdığında, bekleyen goroutine’in Lock çağrısı geri
döner ve goroutine devam eder. Bu, dört durma sınıfından beklediği geldi‘nin bir
örneği: bekleyen goroutine kendi başına durmuyor, başka bir goroutine’in attığı bir adım
— burada kilidi bırakmak — onu serbest bırakıyor. Birden çok goroutine aynı kilidi
beklerken hangisinin önce devam edeceği konusunda sync.Mutex bir sıra garantisi
vermiyor; bu, İşletim Sistemi Kavramları kursunun açlık bahsiyle aynı ailede bir konu ve
burada yalnız bir cümleyle anılıyor, çünkü bu dersin ölçtüğü tek bekleyen için önemsiz.
// sayac.go — dislama kilidiyle korunan sayac ve kilit sinirini modelleyen yari korumali sayac
package main
import "sync"
// KorumaliSayac, dislama kilidiyle korunan bir tamsayi tasir.
type KorumaliSayac struct {
mu sync.Mutex
deger int
}
func (s *KorumaliSayac) Artir() {
s.mu.Lock()
defer s.mu.Unlock()
s.deger++
}
// YariKorumali, artis islemini ikiye ayirir: okuma kilit icinde, yazma kilit disinda.
// Bu, kilit kapsaminin disinda kalan tek satirin modelidir.
type YariKorumali struct {
mu sync.Mutex
deger int
}
func (y *YariKorumali) OkuVeKilidiBirak() int {
y.mu.Lock()
defer y.mu.Unlock()
return y.deger
}
func (y *YariKorumali) YazKilitsiz(yeni int) {
y.deger = yeni
}
KorumaliSayac sözleşmeyi doğru kuruyor: Artirin okuduğu ve yazdığı her şey Lock ile
Unlock arasında kalıyor. YariKorumali ise bilerek bozuk kuruldu — okuma kilit altında,
yazma kilidin tamamen dışında, iki ayrı yöntemde. Bu ikinci tip aşağıdaki sınırlayıcı ölçüm
için; ilk ölçüm yalnız KorumaliSayacı kullanıyor.
Üç Ölçüm, Bir Koşum
// main.go — kilit kapsami, kilitli-kilitsiz karsilastirma, kilit sinirinin modellenmesi
package main
import (
"fmt"
"sync"
)
func main() {
fmt.Println("-- kilidi bekleyen goroutine, kilit birakilinca duruyor --")
var kilit sync.Mutex
rapor := make(chan string, 1)
kilit.Lock()
go func() {
kilit.Lock()
kilit.Unlock()
rapor <- "durdu: beklediği geldi"
}()
kilit.Unlock()
fmt.Println(<-rapor)
fmt.Println()
fmt.Println("-- kilitli ve kilitsiz kurulumda sonuc --")
const goroutineSayisi = 20
const artisSayisi = 100
kilitli := &KorumaliSayac{}
var grup1 sync.WaitGroup
grup1.Add(goroutineSayisi)
for i := 0; i < goroutineSayisi; i++ {
go func() {
defer grup1.Done()
for j := 0; j < artisSayisi; j++ {
kilitli.Artir()
}
}()
}
grup1.Wait()
paylar := make([]int, goroutineSayisi)
var grup2 sync.WaitGroup
grup2.Add(goroutineSayisi)
for i := 0; i < goroutineSayisi; i++ {
go func(indeks int) {
defer grup2.Done()
for j := 0; j < artisSayisi; j++ {
paylar[indeks]++
}
}(i)
}
grup2.Wait()
kilitsizToplam := 0
for _, p := range paylar {
kilitsizToplam += p
}
fmt.Printf("kilitli toplam : %d\n", kilitli.deger)
fmt.Printf("kilitsiz toplam : %d\n", kilitsizToplam)
fmt.Printf("ikisi ayni mi : %v\n", kilitli.deger == kilitsizToplam)
fmt.Println()
fmt.Println("-- sinirlayici olcum: kilit kapsami disina tasan tek satir modelleniyor --")
yarim := &YariKorumali{}
adim1 := make(chan int)
adim2 := make(chan struct{})
bitti := make(chan struct{})
go func() {
eski := yarim.OkuVeKilidiBirak()
adim1 <- eski
<-adim2
yarim.YazKilitsiz(eski + 1)
close(bitti)
}()
eskiDeger := <-adim1
yarim.mu.Lock()
araDeger := yarim.deger
yarim.deger = araDeger + 100
yarim.mu.Unlock()
close(adim2)
<-bitti
fmt.Printf("g1 okudugu eski deger : %d\n", eskiDeger)
fmt.Printf("iki artis uygulandi (+1 ve +100), beklenen toplam: 101, gerceklesen: %d\n", yarim.deger)
}
-- kilidi bekleyen goroutine, kilit birakilinca duruyor -- durdu: beklediği geldi -- kilitli ve kilitsiz kurulumda sonuc -- kilitli toplam : 2000 kilitsiz toplam : 2000 ikisi ayni mi : true -- sinirlayici olcum: kilit kapsami disina tasan tek satir modelleniyor -- g1 okudugu eski deger : 0 iki artis uygulandi (+1 ve +100), beklenen toplam: 101, gerceklesen: 1
PD1. İlk satır ortak tanımın kendi ölçüsünü doğrudan koşturuyor: main, kilidi
tutarken bir goroutine başlatıyor, o goroutine Lock çağrısında bekliyor, main
Unlock çağırınca goroutine devam edip durma sebebini bildiriyor. Goroutine ve Kanallar
dersinde ölçülen 2/5/2/3 dağılımının “beklediği geldi” satırlarından biri tam olarak bu
düzenek — kilit burada beşinci düzenektir.
PD2. İkinci ölçüm yirmi goroutine’i, her biri yüz kez artırarak, iki ayrı yolla
koşturuyor. kilitli kurulumda hepsi aynı KorumaliSayacı paylaşıyor ve her artış
Lock/Unlock çiftinin arasında kalıyor; sonuç deterministik biçimde 2000. kilitsiz
kurulumda ise her goroutine kendi indeksine ait ayrı bir dilim gözesini artırıyor — yirmi
goroutine yirmi farklı bellek konumuna yazıyor, hiçbiri aynı konuma dokunmuyor. İki
kurulum da 2000 veriyor ve bu iki sayı aynı çünkü ikisinde de gerçekte kilit gerektiren
bir yarış hiç kurulmadı: kilitli kurulumda kilit fazladan bir koruma sağlıyor ama zaten
paylaşılmayan bir kaynağı koruyor değil, kilitsiz kurulum da paylaşım yerine ayrık bellek
kullanarak yarışı baştan dışlıyor. Sonuçların eşit çıkması “kilitsiz de güvenlidir”
anlamına gelmiyor; yalnızca bu iki kurulumun ikisinin de güvenli olduğu, ama farklı
sebeplerle güvenli olduğu anlamına geliyor. Gerçekten paylaşılan tek bir değişkene
kilitsiz yazan goroutine’lerin ürettiği sonucu bu derste koşturmuyoruz — o karşılaştırma
bu kursun ikinci sert kısıtına giriyor ve yalnız saptayıcının verdiği yanıtla ölçülüyor.
Kilidin kendisinin bir bedeli var: yirmi goroutine aynı kilit için sırayla bekliyor, yani
her Artir çağrısı bir öncekinin bıraktığı kilidi devralıyor. Bu çekişme — birden çok
goroutine’in aynı kilide aynı anda ulaşmaya çalışması — kilitli kurulumu kilitsiz
kurulumdan yavaşlatabilir, çünkü kilitsiz kurulumda hiçbir goroutine bir diğerini
beklemiyor. Ama bu ders süre ölçmüyor; ölçülen tek şey doğruluk, iki kurulumun da aynı
toplama ulaşması. Çekişmenin bedeli gerçek olsa da, bu dersin sorusunun dışında kalıyor —
kilidin var oluş sebebi hız değil, doğruluk.
Sınırlayıcı Ölçüm: Kilit Kapsamının Sınırı
PD3. YariKorumali, KorumaliSayacla aynı kilidi taşıyor ama sözleşmeyi bozuyor:
değeri okuyup kilidi hemen bırakıyor, sonra kilidin dışında yeni değeri yazıyor.
Ölçüm bu boşluğu rastgele bir zamanlamaya bırakmıyor — iki kanal (adim1, adim2) araya
girerek sırayı kesin biçimde zorluyor: goroutine önce eski değeri okuyup bildiriyor,
main bu bildirimi aldıktan sonra kendi kilidini alıp değeri 100 artırıyor ve bırakıyor,
ancak o zaman goroutine’e devam etmesi için sinyal veriliyor. Goroutine devam ettiğinde
kilidin dışındaki YazKilitsiz çağrısı, kendi okuduğu eski değere göre hesapladığı
eski + 1i yazıyor — mainin araya girip yazdığı 100’ü hiç görmeden üzerine yazıyor.
İki artış birden uygulandı (biri +1, biri +100) ama sonuç 101 değil, 1.
Bu bir yarış koşulunun koşturulması değil, tam tersi: iki adım arasındaki sıra kanallarla
tamamen belirlendiği için mainin yazması ile goroutine’in yazması arasında hiçbir
zamanlama belirsizliği yok, ikisi de her koşumda aynı sırayla, aynı şekilde gerçekleşiyor.
Yarış koşulu saptayıcısı bu koda bakıp herhangi bir uyarı vermez — okuma ve yazma
arasında kanal üzerinden kurulan bir sıra ilişkisi var, iki goroutine aynı anda aynı
belleğe dokunmuyor. Kusur bir zamanlama hatası değil, bir tasarım hatası: kilit
kapsamı, “değeri koru” sözleşmesinin gerektirdiği yerin tamamını kapsamıyor. Değeri okumak
ile yazmak arasında geçen tüm işlem — burada eski + 1 hesabı ve onu geri yazma — kilidin
altında kalmalıydı; kilit yalnızca okuma anını korudu, kararı verme ve geri yazma anını
korumadı.
Bu, dersin sınırlayıcı okuması: kilit kapsamı kadar korur, ne bir satır fazla ne bir
satır eksik. Korunması gereken şey tek bir değişken değil, o değişken üzerindeki bütün
bir işlemdir — oku, hesapla, yaz üçlüsü aynı Lock/Unlock çifti içinde kalmadığı sürece
kilidin varlığı hiçbir şey garanti etmiyor. KorumaliSayac.Artir bunu doğru yapıyor: okuma,
artırma ve yazma tek bir ifadede (s.deger++) ve tek bir kilit kapsamında. YariKorumali
aynı üç işlemi iki ayrı yönteme, iki ayrı kilit kapsamına bölerek sözleşmeyi kırıyor —
kilidin kendisi hiçbir hata vermiyor, çünkü kilidin görevi yalnızca kapsamını korumak,
kapsamın doğru çizilip çizilmediğine karar vermek değil.
YariKorumalinin iki yöntemi ayrı ayrı okunduğunda hiçbiri şüpheli görünmüyor:
OkuVeKilidiBirak kilidi doğru kullanıyor, YazKilitsiz de kendi başına bakıldığında bir
sorun taşımıyor, çünkü tek bir alana atama yapıyor. Kusur ancak iki yöntemin birlikte
çağrılma biçiminde ortaya çıkıyor — aralarında kilidin bırakılıp yeniden alınabileceği bir
boşluk var, ve bu boşluk kaynakta okunarak görülemiyor, yalnız iki çağrının arasına başka
bir goroutine’in girip giremeyeceği sorularak görülebiliyor. Bu, paylaşılan durumu
tasarlarken tekrar eden bir kalıp: bir türün dışa açtığı yöntemler “oku” ve “yaz” gibi ayrı
parçalara bölündüğünde, o parçaları çağıran kod parçaların arasında kilidin bırakıldığını
bilmeyebilir. KorumaliSayac.Artirin tek bir yöntemde kalması bilinçli bir seçim: dışa
açılan sözleşme “bir artışı bölünmez biçimde uygula” oluyor, çağıran taraf okuma ile yazma
arasındaki boşluğu hiç görmüyor çünkü öyle bir boşluk kaynakta hiç yok.
Kilit mi, Kanal mı
Önceki konu boyunca her paylaşım bir kanaldan geçirilerek kuruldu: bir değeri değiştirmek
isteyen goroutine, değeri tutan tek bir goroutine’e bir mesaj gönderiyor, değişikliği o
goroutine kendi sırasında uyguluyordu. Bu ders aynı işi kilitle yapıyor: değeri tutan
KorumaliSayac, kendi kilidini taşıyor ve değişikliği isteyen her goroutine doğrudan o
belleğe erişiyor, aralarına bir aracı goroutine girmiyor. İkisi de aynı sonucu verebilir —
önceki konudaki işçi havuzu kalıbı bunu bir kanalla sağlamıştı, bu ders aynı korumayı bir
kilitle sağlıyor. Ortak tanımın üçüncü iddiası burada tekrar geçerli: durduran şey kanal ya
da kilidin kendisi değil, birinin attığı bir adım; iletişim ile paylaşım arasındaki seçim
bir kurtuluş değil, bir tercih.
Bu tercihin yönü basit bir soruyla bulunabilir: korunan şey bir bellek konumu mu, yoksa bir
sıradaki iş mi? KorumaliSayac.deger yalnız bir tamsayı, üzerinde yapılan tek işlem
artırma; bu işlemi bir kanal üzerinden tek bir sahibe yönlendirmek, doğrudan kilitlemekten
daha fazla dolayım gerektirir ama fazladan bir garanti eklemez. Değer daha karmaşık bir
durumsa — birden çok alanın birlikte tutarlı kalması gerekiyorsa, ya da işlemler arasında
bir sıra kurulması gerekiyorsa — kanalın tek sahipliği daha doğal bir sınır çizer. Bu ders
yalnız kilidin kendi sözleşmesini ölçüyor; iki yaklaşımın hangi durumda tercih edileceği
kursun kapanışına bırakılan daha geniş bir soru.
Özet
sync.Mutex, Go’nun karşılıklı dışlamayı gerçekleştiren kilidi;LockileUnlockarasında kalan satırlar kilidin kapsamını oluşturuyor ve yalnız bu kapsam korunuyor.- Kilidi bekleyen bir goroutine, kilidi tutan başka bir goroutine
Unlockçağırınca devam ediyor — dört durma sınıfından beklediği geldi’nin bir örneği. - Aynı sayaç kilitli ve kilitsiz iki kurulumla 2000’e ulaşıyor; eşitlik güvenlik kanıtı değil, çünkü kilitsiz kurulum paylaşılan tek bir değişkene değil ayrık bellek gözelerine yazarak yarışı zaten dışlıyor.
- Kilit kapsamı dışına taşan tek bir yazma, kanalla zorlanmış bir sırayla modellendi: iki artış uygulandı ama sonuç 101 yerine 1 çıktı, çünkü kilit yalnız okumayı korudu.
- Kilit, korunması gereken bütün işlemi — okuma, hesaplama, yazma — aynı kapsamda tutmayı gerektiriyor; kapsamın nereye çizildiği kilidin kendisinin denetleyemediği bir karar.
Sonraki Adım
Bu derste tek bir goroutine’in kilit bekleyip beklemediği ölçüldü. Ama çoğu paylaşılan
durum sorunu tek bir bekleyenle sınırlı değil: bir goroutine grubunun tamamının
bitmesini beklemek, her birinin ayrı ayrı bir kilit bırakmasını beklemekten farklı bir
sözleşme gerektiriyor. Sıradaki ders bu toplu beklemeyi kuran aracı — sayaç tabanlı
sync.WaitGroupı — ve onun kendi sözleşmesini, sayacın ne zaman sıfırlanacağını ve
sıfırlanmazsa ne olacağını ölçüyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.