Ders 14 / 14
Yarış Koşulu Saptayıcı
Saptayıcı dilin kendi dağıtımının parçası ve program içinden çağrılıyor: kurgulanmış bir kaynak geçici bir dizine yazılıp saptayıcı seçeneğiyle koşturuluyor, yalnız çıkış kodu okunuyor. Üç kurulumdan korumasız paylaşım buluyor, kilitli paylaşım ve kanal üzerinden aktarım bulamıyor. Sınırlayıcı ölçüm — kursun son ve en önemli sınırı — aynı korumasız kod hiç koşmayan bir dalın içine konduğunda saptayıcının onu da bulamadığını gösteriyor: saptayıcı yalnız koşan yolu görür.
İçindekiler
Üç ders boyunca paylaşılan durum kilitle, bekleme grubuyla, bir kez çalıştırmayla ve atomik işlemlerle korundu — ama her ölçüm korumanın var olduğunu baştan biliyordu, çünkü kaynağı biz yazdık. Gerçek bir kaynak tabanında korumanın unutulup unutulmadığını, kaynağı okuyarak değil, kim söylüyor? Bu son ders bu soruyu dilin kendi dağıtımıyla gelen yarış koşulu saptayıcısına soruyor.
Bu kursun ikinci sert kısıtı üç ders boyunca yarış koşulunun hiç koşturulmamasını
zorunlu kıldı — modellenerek ya da ayrık bellekle dışlanarak. Bu son derste bu kısıt
tersine dönüyor: saptayıcının kendisini ölçebilmek için ona gerçekten yakalayabileceği
kurgulanmış bir yarış vermek gerekiyor. Ölçülen şey yine yarışın sonucu değil —
sayacın hangi değere ulaştığı hiçbir yerde okunmuyor — yalnız saptayıcının verdiği tek
bitlik yanıt: buldu mu, bulamadı mı.
Saptayıcı Programın İçinden Nasıl Çağrılır
Saptayıcı, Go Komut Ailesi dersinde ölçülen derleyici, biçimlendirici ve inceleyici gibi dilin kendi dağıtımının bir üyesi ve aynı yordamla çağrılıyor: kurgulanmış kaynak geçici bir modüle yazılıyor, komut o dizinde çalıştırılıyor, sonuç okunuyor. Fark, okunan sonucun ne olduğunda — burada ne bir çıktı metni ne bir biçim farkı okunuyor, yalnız çıkış kodu. Saptayıcı bir yarış bulduğunda çıkış kodu sıfırdan farklı oluyor; bulamadığında program normal biçimde sıfırla kapanıyor.
Saptayıcı, derleyiciden ve inceleyiciden farklı olarak kaynağı yalnız okuyup bir karar vermiyor; kaynağı çalıştırıyor ve çalışırken her bellek okuma ile yazmasını izliyor. Bu izleme çağrının kendisine bir seçenek olarak veriliyor — komut normalde çalıştırdığı programı hiç izlemeden koşturuyor, saptayıcı seçeneği verildiğinde her goroutine’in her bellek erişimine bir gözlemci ekleyerek koşturuyor. Bu gözlem bir bedelle geliyor: aynı program saptayıcıyla koşarken normalden daha yavaş çalışıyor ve daha çok bellek kullanıyor, çünkü her erişimin kim tarafından, ne zaman yapıldığı ayrıca kaydediliyor. Bu ders bu bedeli hiç ölçmüyor — kursun süre yasağı burada da geçerli — yalnız saptayıcının verdiği ikili yanıtı okuyor.
// araclar/araclar.go — yaris kosulu saptayicisini program icinden cagiran ortak yordam
package araclar
import (
"os"
"os/exec"
"path/filepath"
)
// SaptayiciBulduMu, verilen kaynagi gecici bir modulde saptayiciyla kosturur; saptayici
// bir yaris buldugunda cikis kodu sifirdan farkli olur. Saptayicinin kendi ciktisi hicbir
// yere yazilmiyor.
func SaptayiciBulduMu(kaynak string) bool {
dizin, _ := os.MkdirTemp("", "sinama")
defer os.RemoveAll(dizin)
os.WriteFile(filepath.Join(dizin, "go.mod"), []byte("module sinama\n\ngo 1.24\n"), 0o644)
os.WriteFile(filepath.Join(dizin, "main.go"), []byte(kaynak), 0o644)
cmd := exec.Command("go", "run", "-race", ".")
cmd.Dir = dizin
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
return cmd.Run() != nil
}
SaptayiciBulduMunun kendi çıktıyı hiç okumaması bilinçli: cmd.Stdout ile cmd.Stderr
hiç atanmıyor, bu yüzden saptayıcının yazdığı her şey boş aygıta gidiyor. Bu, önceki
derslerdeki panik metninin basılmamasıyla aynı gerekçeye dayanıyor — saptayıcının çıktısı
gerçek bellek adresleri, gerçek goroutine numaraları ve bir yığıt izi taşıyor; ölçülen tek
şey cmd.Run()ın döndürdüğü hata, yani komutun çıkış kodu sıfır mıydı değil miydi.
Alt süreç GOTOOLCHAIN=local ile çağrılıyor, tıpkı Go Komut Ailesi dersindeki gibi — ağdan
bir araç zinciri inmesin diye.
Üç Kurulum, Saptayıcının Verdiği Yanıt
// main.go — uc kurulumda saptayicinin verdigi yanit, ve hic kosmayan bir yoldaki yarisin gorunmezligi
package main
import (
"fmt"
"ders/araclar"
)
func main() {
korumasiz := `package main
func main() {
var sayac int
bitti := make(chan struct{}, 2)
go func() { sayac++; bitti <- struct{}{} }()
go func() { sayac++; bitti <- struct{}{} }()
<-bitti
<-bitti
}
`
kilitli := `package main
import "sync"
func main() {
var sayac int
var kilit sync.Mutex
bitti := make(chan struct{}, 2)
go func() {
kilit.Lock()
sayac++
kilit.Unlock()
bitti <- struct{}{}
}()
go func() {
kilit.Lock()
sayac++
kilit.Unlock()
bitti <- struct{}{}
}()
<-bitti
<-bitti
}
`
kanalli := `package main
func main() {
kanal := make(chan int)
go func() { kanal <- 1 }()
<-kanal
}
`
kosmayanYol := `package main
func main() {
var sayac int
bitti := make(chan struct{}, 2)
kosmayanDal := false
if kosmayanDal {
go func() { sayac++; bitti <- struct{}{} }()
go func() { sayac++; bitti <- struct{}{} }()
<-bitti
<-bitti
}
_ = sayac
}
`
fmt.Println("-- uc kurulumda saptayicinin verdigi yanit --")
fmt.Printf("korumasiz paylasim : buldu=%v\n", araclar.SaptayiciBulduMu(korumasiz))
fmt.Printf("kilitli paylasim : buldu=%v\n", araclar.SaptayiciBulduMu(kilitli))
fmt.Printf("kanal uzerinden aktarim : buldu=%v\n", araclar.SaptayiciBulduMu(kanalli))
fmt.Println()
fmt.Println("-- sinirlayici olcum: hic kosmayan yoldaki yaris gorunmuyor --")
fmt.Printf("kosmayan daldaki yaris : buldu=%v\n", araclar.SaptayiciBulduMu(kosmayanYol))
}
-- uc kurulumda saptayicinin verdigi yanit -- korumasiz paylasim : buldu=true kilitli paylasim : buldu=false kanal uzerinden aktarim : buldu=false -- sinirlayici olcum: hic kosmayan yoldaki yaris gorunmuyor -- kosmayan daldaki yaris : buldu=false
PD12. Üç kaynak birbirinden yalnız korumanın biçimiyle ayrılıyor. korumasiz, iki
goroutine’in aynı sayaca hiç korumasız yazması — birinci dersin KorumaliSayacının
kilitsiz bırakılmış hâli, ama bu kez gerçekten paylaşılan tek bir belleğe gerçekten aynı
anda yazılıyor. kilitli aynı yazmayı bir sync.Mutex ile sarıyor. kanalli ise hiçbir
paylaşılan belleğe dokunmuyor, değeri bir kanaldan geçiriyor. Saptayıcı yalnız birincide
true veriyor — kilitli paylaşım ile kanal üzerinden aktarım, iki ayrı yöntemle, ikisi de
saptayıcının gözünde temiz.
kanallinin temiz çıkması bu kursun ilk dersinden beri kurulan bir örüntüyü doğruluyor:
bir kanaldan geçen değer kopyalanıyor ve gönderen taraf değeri gönderdikten sonra ona bir
daha dokunmuyor, bu yüzden iki goroutine’in aynı belleğe aynı anda erişmesi gibi bir durum
hiç kurulmuyor. kilitlinin temiz çıkması da birinci dersin doğrudan bir doğrulaması:
Lock/Unlock çifti arasına alınan tek bir satır, saptayıcının da onayladığı gibi, iki
goroutine’in aynı anda çalışmasını gerçekten engelliyor. Üç ders boyunca yazıyla anlatılan
şey burada saptayıcının kendi gözlemiyle doğrulanıyor.
Bu üç sonuç, üç ders boyunca modellenerek ya da ayrık bellekle dışlanan yarışın nihayet
gerçekten var olduğu tek yer: korumasiz kurulumu, ilk dersin kilitsiz sayacından farklı
olarak, otuz ayrı goroutine yerine yalnız iki goroutine kullanıyor ve her ikisi de aynı
sayac++ işlemine aynı anda giriyor — okuma, artırma ve yazmanın üç ayrı adımı arada
kesilebiliyor. Saptayıcı bu kesilmeyi çalışma zamanında her bellek erişimini izleyerek
yakalıyor; kilitli ve kanallı kurulumlarda böyle bir kesilme hiç yaşanmadığı için saptayıcı
hiçbir şey bulmuyor, kaynağın “temiz” olduğunu değil, bu koşumda hiçbir ihlal
görmediğini bildiriyor.
Sınırlayıcı Ölçüm: Saptayıcı Koşan Yolu Görür
PD13. kosmayanYol, korumasiz ile birebir aynı yarışı taşıyor — aynı iki goroutine,
aynı korumasız sayac++. Tek fark, bu goroutine’lerin kosmayanDal adlı, hiçbir zaman
true olmayan bir koşulun içine yerleştirilmiş olması. Saptayıcı bu kaynağı da
buldu=false diye yanıtlıyor — kaynakta bir yarış var, ama saptayıcı onu bulamıyor,
çünkü o goroutine’ler bu koşumda hiç başlamadı. Saptayıcı kaynağı okuyup bir tehlike
analizi yapmıyor; yalnız çalışan programı gözlemliyor ve gözlemlemediği bir bellek
erişimini hiçbir biçimde göremiyor.
Bu, kursun kapanış sınırı: saptayıcı bir kanıt değil, bir gözlem aracı. Bir kaynağın
saptayıcıdan buldu=false ile geçmesi, o kaynakta yarış olmadığını değil, o koşumda
çalışan yolların hiçbirinde yarış görülmediğini söylüyor. kosmayanYol bu farkı en açık
biçimde gösteriyor — kaynak birebir aynı tehlikeyi taşırken yanıt tersine dönüyor, çünkü
tehlikeli dal bu çalıştırmada hiç seçilmedi. Bir kod tabanında nadiren giren bir dal, bir
hata işleme yolu, ya da bir yapılandırmaya bağlı bir dal, saptayıcının hiç görmediği bir
yarışı taşıyabiliyor — testler o dalı koşturmadığı sürece.
Bu sınır saptayıcıyı gereksiz kılmıyor, yalnız neyi garanti ettiğini daraltıyor. Saptayıcı
buldu=true dediğinde bu kesin bir kanıt — kurgulanmış korumasiz kaynağında olduğu gibi,
gerçekten bir ihlal gözlendi. Ama buldu=false bir garanti değil, yalnız o koşumun bir
raporu. Bu, testin genel doğasıyla aynı sınıra giriyor: bir testin geçmesi kodun doğru
olduğunu kanıtlamıyor, yalnız test edilen yolların o koşumda beklenen davranışı verdiğini
gösteriyor — saptayıcı bu sınırı eşzamanlılık için bir kez daha kuruyor, çalışmayan bir
dal ne kadar iyi yazılmış olursa olsun hiç sınanmamış demektir.
Kurs Kapanışı
On dört ders boyunca bu kursun tek sorusu şuydu: bir goroutine nasıl duracak? Ortak tanımın üç iddiası bu soruya üç yanıt verdi. Birincisi: bir goroutine başlatmak, onun biteceği anlamına gelmiyor — on iki goroutine’in yalnız ikisi kendi durdu, geri kalanı başka bir goroutine’in attığı bir adıma bağlıydı. İkincisi: durmamak hata vermiyor — üç sızıntının üçü de sessizdi, program normal çıkışla kapandı. Üçüncüsü, bu kursa özgü olanı: durduran şey kanalın kendisi değil, birinin yaptığı bir iş — kilit, bekleme grubu, bir kez çalıştırma, hepsi aynı bağımlılığı farklı bir düzenekle kuruyor.
Bir sınırlayıcı okuma ve bir kör nokta bu üç iddiayı tamamlıyor. Sınırlayıcı okuma: “kendi durdu” da bir sözleşme, çünkü gövdesi biten bir goroutine bunu kimseye kendiliğinden bildirmiyor — bildirmesi için yine bir kanal ya da bir sayaç gerekiyor. Kör nokta: sızıntının sayısı hiçbir derste ölçülmedi, yalnız varlığı boolean olarak ölçüldü, çünkü çalışma zamanının kendi goroutine sayısı taban bir değere göre karşılaştırılabiliyordu, gerçek sayı değil.
Bu son ders dördüncü bir gözlemi ekliyor: korumanın var olup olmadığını kaynağı okuyarak söylemek güç, ama saptayıcı da bunu tam olarak söylemiyor — yalnız koştuğu yolda bir ihlal görüp görmediğini söylüyor. Kurs aşırı bir genelleme yapmadan kapanıyor: bu on dört ders Go’nun eşzamanlılık modelinin tamamını kapsamıyor, yalnız bir goroutine’in nasıl durduğu ve bir paylaşımın nasıl korunduğu sorularına, dört düzenek ve bir saptama aracı üzerinden, ölçülebilir yanıtlar verdi.
| Ders | Ölçülen durma biçimi | Sınıfı (kendi · beklediği geldi · iptal · hiç durmadı) | Sınırlayıcı ölçüm |
|---|---|---|---|
| Goroutine | On iki goroutine, dört durma biçimi (2/5/2/3 dağılımı) | kendi · beklediği geldi · iptal · hiç durmadı (hepsi) | “Kendi durdu” da bir sözleşme — bildirim gerekir |
| Kanallar | Tamponsuz kanalda gönderim-alım buluşması | beklediği geldi | Kanal bir kuyruk değil, değer kopyalanır |
| Tamponlu ve Tamponsuz Kanallar | Kapasiteye göre engellemenin ertelenmesi | beklediği geldi | Tampon eşzamanlamayı erteler, kaldırmaz |
select Deyimi |
Birden çok kanal arasından hazır olanın seçimi | beklediği geldi | Aynı anda hazır dallarda seçim rastgele, koşturulmadan modellendi |
| Kanal Kapatma ve Aralık | Kapanan kanaldan alımın sıfır değer ve false vermesi |
beklediği geldi | Kapalı kanala gönderim ve ikinci kapatma panikler, yalnız panik=true/false yazılır |
| İşçi Havuzları | İş kanalı kapanınca işçilerin durması | beklediği geldi | İşçi sayısını artırmak işi bölmez; kapanmayan kanalda sızıntı |
| Yayma ve Toplama | Yayılan işin toplama kanalında birleşmesi, toplayıcının durma sebebi | beklediği geldi | Toplama kanalını yalnız biri kapatabilir; kapatılmazsa toplayıcı hiç durmaz |
| Boru Hatları | Her aşamanın kendi kanalını açıp kapatarak zinciri sürdürmesi | beklediği geldi | Tüketici erken vazgeçtiğinde önündeki aşamalar hiç durmaz |
| Bağlam Paketi | Tek bir iptal, ctx.Done() üzerinde bekleyen goroutine’lerin hepsine aynı anda ulaşıyor |
iptal | Bağlam kimseyi zorla durdurmaz, yalnız haber verir; Done()u denetlemeyen goroutine işini sürdürür |
| Zaman Aşımı ve Son Tarihler | Zaman aşımı bir select dal seçimidir; süre değil, seçilen dal ölçülür |
iptal (hazır olan dal) | Bekleyeni kurtarır ama beklenen işi durdurmaz; iş goroutine’i koşmaya devam eder ve sızıntı boolean’ı bunu gösterir |
| Dışlama Kilitleri | Kilit bırakılınca bekleyenin devam etmesi | beklediği geldi | Kilit yalnız kapsamı korur, kapsam dışı korumasız kalır |
| Bekleme Grupları | Sayaç sıfıra inince Waitin dönmesi |
beklediği geldi | Sıra garanti edilmez, yalnız hepsinin bittiği garanti edilir |
| Eşzamanlılık İlkelleri | Bir kez çalıştırmanın gövdeyi bir kez koşturması | beklediği geldi | Atomik işlem tek değişkeni korur, ikisi birden koşturulmadan modellendi |
| Yarış Koşulu Saptayıcı | Saptayıcının üç kurulumdaki yanıtı | kendi (korumasız/kilitli) · beklediği geldi (kanallı) | Saptayıcı yalnız koşan yolu görür, koşmayan yoldaki yarışı bulamaz |
Özet
- Saptayıcı dilin kendi dağıtımının bir üyesi ve program içinden çağrılıyor; kurgulanmış bir yarış geçici bir modülde saptayıcı seçeneğiyle koşturulup yalnız çıkış kodu okundu.
- Üç kurulumdan yalnız korumasız paylaşım
buldu=trueverdi; kilitli paylaşım ile kanal üzerinden aktarım saptayıcının gözünde temiz çıktı — üç ders boyunca yazıyla anlatılan ayrımı doğrudan doğruladı. - Aynı korumasız kaynak hiç koşmayan bir dalın içine konduğunda saptayıcı onu bulamadı: saptayıcı bir kanıt değil, yalnız o koşumda çalışan yolların bir gözlemi.
- Ortak tanımın üç iddiası — kendi durmak azınlıkta, durmamak sessiz, durduran şey birinin yaptığı bir iş — on dört ders boyunca dört ayrı düzenekle doğrulandı.
- Kurs on dört dersin ölçtüğü yanıtları tek bir tabloda topladı; her satır kendi kaynak dersinden okundu.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.