Ders 13 / 14
Eşzamanlılık İlkelleri
Otuz goroutine aynı ilklendirmeyi çağırıyor ama bir kez çalıştırma sözleşmesi gövdenin tam olarak bir kez koşmasını garanti ediyor. Otuz goroutine, her biri beş yüz artışla, aynı sayacı bir kez atomik işlemlerle bir kez kilitle artırıyor; ikisi de 15000 veriyor. Sınırlayıcı ölçüm, iki ayrı atomik değişkenin (bakiye ve işlem sayısı) her biri kendi başına tutarlı kalırken bir arada okunduklarında tutarsız bir çift verebileceğini kanalla zorlanmış bir sırayla modelliyor: okuyan taraf bakiye=100, işlemSayısı=0 görüyor.
İçindekiler
Önceki dersin kapanışı bir sınır çizdi: kilit iki ders boyunca paylaşılan durumu korumanın
tek aracıymış gibi göründü, ama değil. Bu ders aynı korumayı iki ayrı ilkelle kuruyor —
sync.Once, bir gövdenin kaç kez çağrılırsa çağrılsın yalnız bir kez koşmasını garanti
eden bir kez çalıştırma; ve sync/atomic, tek bir değişken üzerindeki işlemleri
kilit kullanmadan güvenli kılan atomik işlemler. İkisi de kilidin yaptığı işi
yapıyor gibi görünüyor, ama ikisinin de sözleşmesi kilitten daha dar: kilit herhangi bir
kod parçasını sarabiliyordu, bu iki ilkelin her biri yalnız tek bir dar durumu kapsıyor.
Ortak tanımın üçüncü iddiası bu derste de geçerli kalıyor: durduran şey aracın adı değil, başka bir goroutine’in attığı bir adım. Bir kez çalıştırmada bu adım ilk çağrının gövdeyi bitirmesi — bekleyen goroutine’ler kilitteki gibi beklediği geldi sınıfına giriyor. Atomik işlemde ise hiç bekleme yok: atomik bir çağrı hiçbir goroutine’in bir şey yapmasını beklemeden, hiç bloklanmadan döner — kendi başına biten bir adım, kimseye bağlı değil. İkisi de kilidin tek bir dar biçimini temsil ediyor, ama biri hâlâ bekletiyor, öbürü hiç bekletmiyor.
Bir Kez Çalıştırma
Bir programın başlangıcında yalnız bir kez çalışması gereken bir adım — bir bağlantı
havuzunu kurmak, bir yapılandırmayı okumak — birden çok goroutine tarafından aynı anda
istenebilir. sync.Once bu isteği tek bir çağrıya indiriyor: Do yöntemine verilen
işlev, kaç goroutine Doyu çağırırsa çağırsın, ömür boyu tam olarak bir kez koşuyor;
sonraki her çağrı hiçbir şey yapmadan hemen dönüyor. sync.Oncein sıfır değeri de tıpkı
kilit gibi kullanıma hazır: var bir sync.Once yazmak yeterli, ayrı bir kurulum
gerekmiyor — çünkü Oncein içinde, ilk çağrının olup olmadığını izleyen bir sayaç ve o
sayacı koruyan kendi kilidi zaten var. Oncein kendisi, bir önceki dersin ilkelini
kullanarak kurulmuş bir araç.
Aynı bir değişkenine ait Do çağrılarına farklı işlevler verilse bile sonuç
değişmiyor — koşan yalnız ilk çağrının işlevi, sonraki çağrılara verilen işlevler hiç
çalışmadan atlanıyor. Bu örnekte otuz goroutine aynı işlevi verdiği için bu ayrım
görünmüyor, ama sözleşme bunu zaten garanti ediyor: Oncein hafızası “hangi işlev
verildi” değil, yalnız “bir kez çağrıldım mı” sorusuna verilen tek bitlik bir yanıt.
Hangi goroutine’in kendi Do çağrısının gövdeyi gerçekten koşturduğu da ölçülmüyor ve
ölçülemez — otuz goroutine aynı anda Doyu çağırıyor, hangisinin “kazandığı” zamanlamaya
bağlı; ama hepsi aynı işlevi verdiği için bu belirsizlik, birinci dersin kilitsiz
kurulumunda olduğu gibi, sonucu hiç etkilemiyor.
// main.go — bir kez calistirma, atomik sayac ile kilitli sayacin karsilastirilmasi, cift atomik degiskenin modellenmesi
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
fmt.Println("-- bir kez calistirma: N goroutine cagiriyor, govde bir kez kosuyor --")
var bir sync.Once
var kacKezKostu int64
var ilkDeger int64
const goroutineSayisi = 30
var grup sync.WaitGroup
grup.Add(goroutineSayisi)
for i := 0; i < goroutineSayisi; i++ {
go func() {
defer grup.Done()
bir.Do(func() {
atomic.AddInt64(&kacKezKostu, 1)
atomic.StoreInt64(&ilkDeger, 42)
})
}()
}
grup.Wait()
fmt.Printf("govde kac kez kostu: %d\n", kacKezKostu)
fmt.Printf("ilklendirilen deger: %d\n", ilkDeger)
fmt.Println()
fmt.Println("-- atomik sayac ile kilitli sayac ayni sonucu veriyor mu --")
const artisSayisi = 500
var atomikSayac int64
var grup2 sync.WaitGroup
grup2.Add(goroutineSayisi)
for i := 0; i < goroutineSayisi; i++ {
go func() {
defer grup2.Done()
for j := 0; j < artisSayisi; j++ {
atomic.AddInt64(&atomikSayac, 1)
}
}()
}
grup2.Wait()
var kilit sync.Mutex
kilitliSayac := 0
var grup3 sync.WaitGroup
grup3.Add(goroutineSayisi)
for i := 0; i < goroutineSayisi; i++ {
go func() {
defer grup3.Done()
for j := 0; j < artisSayisi; j++ {
kilit.Lock()
kilitliSayac++
kilit.Unlock()
}
}()
}
grup3.Wait()
fmt.Printf("atomik sayac : %d\n", atomikSayac)
fmt.Printf("kilitli sayac : %d\n", kilitliSayac)
fmt.Printf("ikisi ayni mi : %v\n", atomikSayac == int64(kilitliSayac))
fmt.Println()
fmt.Println("-- sinirlayici olcum: iki atomik degiskenin birlikte tutarliligi modelleniyor --")
var bakiye, islemSayisi int64
adim1 := make(chan struct{})
bitti := make(chan [2]int64)
go func() {
<-adim1
okunanBakiye := atomic.LoadInt64(&bakiye)
okunanIslem := atomic.LoadInt64(&islemSayisi)
bitti <- [2]int64{okunanBakiye, okunanIslem}
}()
atomic.AddInt64(&bakiye, 100)
adim1 <- struct{}{}
ara := <-bitti
atomic.AddInt64(&islemSayisi, 1)
fmt.Printf("yazan taraf tamamladiginda bakiye=%d islemSayisi=%d\n",
atomic.LoadInt64(&bakiye), atomic.LoadInt64(&islemSayisi))
fmt.Printf("okuyan tarafin arada gordugu bakiye=%d islemSayisi=%d (tutarsiz cift)\n",
ara[0], ara[1])
}
-- bir kez calistirma: N goroutine cagiriyor, govde bir kez kosuyor -- govde kac kez kostu: 1 ilklendirilen deger: 42 -- atomik sayac ile kilitli sayac ayni sonucu veriyor mu -- atomik sayac : 15000 kilitli sayac : 15000 ikisi ayni mi : true -- sinirlayici olcum: iki atomik degiskenin birlikte tutarliligi modelleniyor -- yazan taraf tamamladiginda bakiye=100 islemSayisi=1 okuyan tarafin arada gordugu bakiye=100 islemSayisi=0 (tutarsiz cift)
PD9. Otuz goroutine aynı bir.Do(...) çağrısını neredeyse aynı anda yapıyor, ama
kacKezKostu yalnız 1. sync.Oncein sözleşmesi burada bekleme grubundan farklı bir
şekilde çalışıyor: bekleme grubu bir sayacı dışarıdan artırıp azaltan çağrılara
dayanıyordu, Once ise kendi içinde bir kez çalıştıysa bir daha hiç çalışmayacağını
kendisi kararlaştırıyor. Otuz çağrıdan yalnız biri gövdeyi gerçekten koşturuyor,
geri kalan yirmi dokuzu Donun döndüğü anı — gövde daha önce koşmuşsa hemen, hâlâ
koşuyorsa gövde bitene kadar — bekliyor. Bu da beklediği geldi sınıfının bir başka
düzeneği: bekleyen goroutine’ler kendi başlarına durmuyor, ilk çağrının gövdeyi bitirmesi
onları serbest bırakıyor.
ilkDegerin çıktısı bu garantinin ikinci yarısını gösteriyor: yalnız gövdenin bir kez
koşması yetmiyor, gövdenin yazdığı değerin her goroutine tarafından görülebilmesi de
gerekiyor. Otuz goroutine’den yalnız biri ilkDegere 42 yazıyor, ama Dodan dönen
her goroutine — kendi çağrısı gövdeyi koşturmuş olsun ya da olmasın — aynı 42yi okuyor.
Bu, sync.Oncein kendi içindeki senkronizasyonun bir sonucu: Dodan dönmek, yalnız
“gövde bir yerde koştu” bilgisini değil, gövdenin yazdığı her değeri de beraberinde
taşıyor.
PD10. İkinci ölçüm aynı toplamı iki ayrı yoldan üretiyor: atomikSayac,
atomic.AddInt64 ile korunuyor ve hiçbir kilide dokunmuyor; kilitliSayac, önceki
dersteki gibi Lock/Unlock çiftiyle korunuyor. Otuz goroutine, her biri beş yüz artışla,
toplam on beş bin artış uyguluyor ve iki sayaç da tam olarak 15000e ulaşıyor. Bu eşitlik
birinci dersteki kilitli/kilitsiz karşılaştırmasından farklı: orada kilitsiz kurulum
paylaşılan tek bir belleğe hiç dokunmuyordu, burada atomikSayac gerçekten paylaşılan tek
bir bellek konumu ve otuz goroutine ona gerçekten aynı anda yazıyor. Atomik işlem
paylaşılan durum yarışını koşturmadan değil, güvenli kılarak dışlıyor — donanımın
sağladığı bölünmez oku-değiştir-yaz işlemiyle, kilit gerekmeden.
M01/K05 İşletim Sistemi Kavramları‘nda kapanan atomik işlem kavramı burada aynen
geçerli: bir işlemin bölünmez olması, araya başka hiçbir goroutine’in giremeyeceği anlamına
geliyor. Go’nun sync/atomic paketi bu bölünmezliği belirli tamsayı ve işaretçi tipleri
için donanım desteğiyle sağlıyor; AddInt64 çağrısının kendisi hiçbir zaman yarım
kalmıyor, bir goroutine okuma ile yazma arasına asla giremiyor.
Bu güvence bir bedelle geliyor: sync/atomicin koruduğu şey yalnız int32, int64,
uint32, uint64, işaretçi gibi belirli birkaç tip, ve her çağrı bu tiplerden tek
birinin üzerinde çalışıyor. Kilit böyle bir sınır tanımıyordu — Lock/Unlock çifti
arasına bir yapı, bir dilim, bir sözlük, ya da birden fazla değişkeni güncelleyen
keyfi uzunlukta bir kod parçası girebiliyordu. Atomik işlemin dar sözleşmesi tam olarak bu
esnekliğin bir kısmından vazgeçmenin karşılığı: tek bir tamsayıyı korumak için kilide hiç
gerek kalmıyor, ama korunan şey bir tamsayının ötesine geçtiği anda atomik işlem yetersiz
kalıyor.
sync/atomic bu derste kullanılan AddInt64 ve LoadInt64in yanında bir de karşılaştırıp
değiştirme çağrısı taşıyor: verilen değişken beklenen değere eşitse yeni değeri yazıyor,
değilse hiçbir şey yapmadan bunu bildiriyor — okuma ile yazma arasına hiçbir goroutine’in
giremeyeceği bir başka bölünmez işlem. Bu ders o çağrıyı koşturmuyor; burada önemli olan
sync/atomicin tek bir tamsayı üzerindeki her işlemi (ekleme, okuma, yazma, koşullu
değiştirme) aynı bölünmezlik garantisiyle sunduğu, ama sunduğu şeyin her zaman tek bir
değişkenle sınırlı kaldığı.
Sınırlayıcı Ölçüm: İki Atomik Değişkeni Birlikte Tutmak
PD11. Üçüncü kurulum tek bir değişken yerine iki tanesini — bakiye ve
islemSayisi — kullanıyor ve ikisi de kendi başına atomik olarak okunup yazılıyor. Ama
aralarında hiçbir birleşik koruma yok: yazan taraf önce bakiyeyi güncelliyor, sonra
islemSayisini; bu iki güncelleme arasında kalan bir an, dışarıdan bakan bir goroutine’e
tutarsız bir çift gösterebiliyor. Bu boşluk kanalla zorlanan bir sırayla modelleniyor:
okuyan goroutine adim1 kanalından sinyal bekliyor, main önce bakiyeyi 100 yapıp
sinyali gönderiyor, okuyan goroutine tam bu anda hem bakiyeyi hem islemSayisini
okuyor — bakiye=100 ama islemSayisi henüz 0. main ancak bundan sonra
islemSayisini artırıyor.
Bu, önceki dersteki kilit kapsamı modeliyle aynı ailede bir sınır, ama farklı bir yerde
duruyor: orada sorun kilidin kapsamının yanlış çizilmesiydi, burada iki değişkenin
hiç ortak bir kapsamı yok — her biri kendi başına atomik, ama ikisi birlikte değil.
bakiye okunduğu an tanımlı bir değer taşıyor, islemSayisi okunduğu an da tanımlı bir
değer taşıyor; okuma sonucu yanlış değil, yalnızca iki okumanın arasına başka bir
goroutine’in bir yazması girebiliyor ve bu, tek başına doğru olan iki değeri bir arada
yanlış bir hikâyeye dönüştürebiliyor. Atomik işlem tek bir değişkeni korumak için var;
birden çok değişkenin birlikte tutarlı kalması gerekiyorsa atomik işlem yetmiyor, o
korumayı yeniden bir kilit üstleniyor.
Bu sınırın kendisi bir tasarım hatası değil, önceki dersteki kilit kapsamı örneğinin bir
genellemesi. Orada sorun tek bir sayacın oku-hesapla-yaz üçlüsünün kilidin dışına
taşmasıydı; burada iki ayrı sayacın ikisi birden tek bir kapsam altında değil. İkisi
de aynı soruyu soruyor: korunması gereken işlem nerede başlayıp nerede bitiyor? bakiye ve
islemSayisi ayrı ayrı atomik olduğu için bu soru kaynağa bakılarak fark edilmiyor —
her iki değişken de kendi satırında doğru görünüyor, aralarındaki ilişkinin korunmadığı
yalnızca iki değişkeni birlikte okumaya çalışan bir kod parçası yazılıp koşturulduğunda
ortaya çıkıyor.
Kilit, Bir Kez Çalıştırma, Atomik İşlem
Üç ders boyunca ölçülen üç araç aynı soruya üç ayrı dar yanıt veriyor. Kilit “hangi kod
parçası aynı anda tek bir goroutine tarafından çalışsın?” sorusuna yanıt veriyor ve bu
kapsamı Lock/Unlock çiftiyle kendi çiziyor — çizim yanlış olursa (birinci dersin
sınırlayıcı ölçümü) kilit bunu fark etmiyor. Bir kez çalıştırma “bu gövde daha önce koştu
mu?” sorusuna yanıt veriyor ve bu soruyu sonsuza dek hatırlıyor; ikinci bir Do çağrısı
gövdeyi bir daha hiç çalıştırmıyor, çünkü soru zaten bir kez yanıtlandı. Atomik işlem “bu
tek değişkene aynı anda kaç goroutine dokunabilir?” sorusuna yanıt veriyor ve yanıtı
donanım düzeyinde garanti ediyor, ama soruyu yalnız tek bir değişken için soruyor.
Üçü de paylaşılan durumu koruyor, ama üçünün kapsamı birbirinin yerine geçmiyor: bir kez çalıştırmayla bir sayacı sürekli korumak, ya da atomik işlemle birden fazla adımı tek bir işlem gibi göstermek mümkün değil. Kod tabanında hangi ilkelin seçildiği, korunması gereken şeyin doğasını — bir olay mı, tek bir değer mi, yoksa keyfi bir kod parçası mı — açığa çıkarıyor. Bu üç dersin ortak sonucu bu: paylaşılan durumu korumanın tek bir doğru yolu yok, korunacak şeyin şekline göre değişen üç ayrı dar sözleşme var.
Bu üç dar sözleşme aynı zamanda birbirine iç içe geçebiliyor. sync.Oncein kendisi bir
kilitle kuruldu; ikinci dersin KorumaliSayacı gibi bir tip, kendi kilidini taşırken
aynı zamanda bir sync.Once alanı taşıyıp yalnız ilk erişimde bir alanı ilklendirebilir.
Atomik işlem de kilidin yerini almıyor, kilidin bir yerine geçebileceği en dar
durumları kilitsiz bırakıyor. Üçü de aynı ölçü ekseninin birer görünümü: bir goroutine’in
paylaşılan bir duruma nasıl eriştiği, o durumun ne kadarını, ne kadar süreyle koruma
altında tuttuğuyla ölçülüyor.
Özet
sync.Once,Doya verilen gövdenin kaç goroutine çağırırsa çağırsın tam olarak bir kez koşmasını garanti ediyor; otuz çağrıdan yalnız biri gövdeyi koşturdu.- Bekleyen goroutine’ler
Oncein gövdesi bitene kadar bloklanıyor — beklediği geldi sınıfının kilit ve bekleme grubundan farklı bir düzeneği. sync/atomic, tek bir değişken üzerindeki işlemleri donanımın bölünmez oku-değiştir-yaz desteğiyle koruyor; otuz goroutine on beş bin artış uyguladı ve atomik sayaç kilitli sayaçla aynı sonucu (15000) verdi.- İki ayrı atomik değişken, her biri kendi başına tutarlı olsa da, birlikte okunduğunda tutarsız bir çift verebiliyor; bu, kanalla zorlanmış bir sırayla modellendi, koşturulmadı.
- Atomik işlem tek bir değişkeni korur; birden fazla değişkenin birlikte tutarlılığı gerekiyorsa kilit kapsamı hâlâ gereken araç.
- Üç ders boyunca ölçülen kilit, bekleme grubu, bir kez çalıştırma ve atomik işlem aynı ölçü ekseninin dört ayrı görünümü: her biri paylaşılan durumun ne kadarını, ne kadar süreyle koruma altında tuttuğuyla tanımlanıyor.
Sonraki Adım
Üç ders boyunca paylaşılan durum kilitle, bekleme grubuyla ve şimdi bir kez çalıştırma ve atomik işlemlerle korundu — ama her ölçüm korumanın var olduğunu baştan biliyordu, çünkü kaynağı biz yazdık. Bu üç dersin hiçbirinde gerçek bir yarış koşulu koşturulmadı: birinci ders bunu ayrık bellekle ve zorlanmış bir modelle, bu ders de zorlanmış bir modelle dışladı. Gerçek bir kaynak tabanında korumanın unutulup unutulmadığını, kaynağı okuyarak değil, kim söylüyor? Kursun son dersi bu soruyu, dilin kendi dağıtımıyla gelen yarış koşulu saptayıcısına soruyor: korumasız bir paylaşım, kilitli bir paylaşım ve kanal üzerinden bir aktarım aynı saptayıcıdan geçiriliyor, saptayıcının verdiği yanıt (buldu mu, bulamadı mı) okunuyor, ve saptayıcının kendi sınırı — hiç çalışmayan bir yoldaki yarışı görememesi — kursun son ölçümü olarak ortaya çıkıyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.