Ders 10 / 14
Zaman Aşımı ve Son Tarihler
Zaman aşımı bir select deyiminin bir dal seçimidir: sonuç önceden hazırsa sonuç dalı, bağlam önceden iptal edilmişse (zaman aşımını temsilen) iptal dalı seçiliyor — ikisi de belirlenimci, çünkü kurulumda yalnız bir dal gerçekten hazır. Süre hiçbir yerde ölçülmüyor, basılan tek şey hangi dalın seçildiği. Sınırlayıcı ölçüm: zaman aşımı bekleyen tarafı kurtarıyor ama beklenen işi durdurmuyor — dalından çıkan çağıranın arkasında, sonucunu artık kimsenin almayacağı bir iş goroutine'i sızıntı olarak kalıyor.
İçindekiler
Önceki ders bir bağlamın iptalinin, o iptal zaten gerçekleştikten sonra bekleyenlere nasıl
ulaştığını ölçtü — iptalEt() açıkça çağrılmıştı, bekleyenler bu çağrıyı bekliyordu. Ama
gerçek sistemlerde iptalin en sık sebebi bir kişinin kararı değil, bir sürenin dolması: bir
ağ çağrısının makul bir sürede yanıt vermesini beklemek, vermezse vazgeçmek. Bu ders zaman
aşımını ölçüyor — ama süre olarak değil. Zaman aşımı, aslında bir select deyiminin iki
dalından birinin seçilmesinden ibaret: sonuç mu geldi, iptal mi geldi. Bu ders süreyi hiç
ölçmeden, hangi dalın seçildiğini belirlenimci biçimde kuruyor.
Bağlam paketi bunun için iki ayrı kurucu sunuyor: context.WithTimeout, şu andan itibaren
göreli bir süre sonra iptal eden bir bağlam kurar; context.WithDeadline ise takvimde
mutlak bir an (bir son tarih) belirtir ve o ana ulaşıldığında iptal eder. İkisi de aynı
sonuca varıyor — belirli bir noktada kendiliğinden kapanan bir Done() kanalı — ve select
açısından ikisi de önceki dersteki context.WithCancelden hiçbir şekilde ayırt edilemiyor.
Bu ders bu yüzden ikisini de ayrı ayrı kurmuyor; ikisinin de vardığı ortak nokta olan “bağlam
bir noktada kapanmış” durumunu modelliyor ve gerçek bir süre ya da gerçek bir takvim değeri
hiç yazdırmıyor.
Bir Dal Seçimi: Sonuç mu, İptal mi
Aşağıdaki ölçüm iki kurulumu yan yana koyuyor. Birincisinde sonuç önceden hazır: select
çalıştığında sonuç kanalında zaten bir değer bekliyor, bağlam iptal edilmemiş. İkincisinde
bağlam önceden iptal edilmiş — zaman aşımını temsilen — ve sonuç kanalı boş, hiçbir
gönderici yok. Her iki kurulumda da selectin hangi dalı seçeceği önceden bellidir, çünkü
kurulumun kendisinde yalnız bir dal gerçekten hazır.
// dalSecimi.go — zaman asimi bir dal secimidir; sure degil hangi dalin secildigi olculur
package main
import (
"context"
"fmt"
)
func dalSec(ctx context.Context, sonuc <-chan string) string {
select {
case deger := <-sonuc:
return "sonuc: " + deger
case <-ctx.Done():
return "iptal: " + ctx.Err().Error()
}
}
func main() {
fmt.Println("-- kurulum 1: sonuc onceden hazir --")
ctxHazir, iptalHazir := context.WithCancel(context.Background())
defer iptalHazir()
sonucHazir := make(chan string, 1)
sonucHazir <- "tamamlandi"
fmt.Println("secilen dal:", dalSec(ctxHazir, sonucHazir))
fmt.Println()
fmt.Println("-- kurulum 2: baglam onceden iptal edilmis (zaman asimini temsil eder) --")
ctxIptal, iptalEt := context.WithCancel(context.Background())
iptalEt()
sonucBos := make(chan string)
fmt.Println("secilen dal:", dalSec(ctxIptal, sonucBos))
}
-- kurulum 1: sonuc onceden hazir -- secilen dal: sonuc: tamamlandi -- kurulum 2: baglam onceden iptal edilmis (zaman asimini temsil eder) -- secilen dal: iptal: context canceled
KL19. Her iki kurulumda da dalSec çağrıldığı anda selectin önünde yalnız bir hazır
dal var — bu, sonucun rastgele seçilmediğinin garantisi. Kurulum 1’de sonucHazir tamponlu
bir kanal ve içine değer select çalışmadan önce konmuş; ctxHazir iptal edilmemiş,
yani ctx.Done() hiç kapanmamış. select, iki daldan yalnız birinin hazır olduğu bir durumda
her zaman o dalı seçiyor — burada seçim şansa değil, kurulumun kendisine bağlı. Kurulum 2’nin
aynası: ctxIptal dalSec çağrılmadan önce iptal edilmiş, sonucBos ise tamamen boş ve
hiçbir goroutine ona yazmıyor — ctx.Done() dalı burada tek hazır dal.
Bu iki kurulum bilerek uç noktalara yerleştirildi: gerçek bir yarışta iki dal da aynı anda
hazır hâle gelebilir (sonuç tam zaman aşımıyla aynı ana denk gelirse), ve o durumda select
hangisini seçeceğine rastgele karar veriyor — bu, önceki derste kanal seçiminde görülen aynı
rastgelelik kuralı. Ama bu ders o ortak noktayı hiç kurmuyor: iki kurulumun ikisi de yalnız
bir dalın hazır olduğu, öbürünün kesinlikle hazır olmadığı durumlar. Belirlenimcilik buradan
geliyor — ölçüm rastgeleliğin olmadığı iki köşeyi seçiyor, rastgeleliğin olabileceği orta
bölgeyi hiç koşturmuyor.
KL20. Gerçek bir zaman aşımı context.WithTimeout ile kurulur ve bağlam belirli bir süre
sonra kendiliğinden iptal olur — bu dersin ölçtüğü kurulumlar o gerçek zamanlamayı
modelliyor, gerçek bir süre geçmesini hiç beklemeden. Kurulum 2’deki ctxIptalin “zaten
iptal edilmiş” olması, “süresi dolmuş bir zaman aşımı bağlamı” ile aynı sonucu üretiyor:
selectin görünen yüzü ikisinde de aynı, çünkü select bir bağlamın neden iptal
edildiğini bilmiyor, yalnız Done() kanalının kapalı olup olmadığını görüyor. Bu, ölçümün
gerçek bir zaman aşımı kadar geçerli olmasını sağlıyor — fark yalnız iptalin ne zaman
tetiklendiğinde, selectin davranışında değil.
Süre burada hiçbir satırda geçmiyor: ne bir time.Sleep, ne bir time.After, ne bir ölçülen
gecikme. Ölçülen tek şey hangi dalın seçildiği — bu, önceki dersteki “süre değil, seçilen
dal yazılır” kuralının doğrudan uygulanışı. time.Sleep ile bir sıra kurmanın neden yasak
olduğu burada bir kez daha görünüyor: eğer bu ölçüm gerçek bir zaman aşımı süresiyle
kurulsaydı, hangi dalın kazanacağı makinenin o anki yüküne bağlı kalırdı — modelleme bu
belirsizliği tamamen ortadan kaldırıyor.
dalSec işlevinin kendisi hem kurulum 1’de hem kurulum 2’de aynı — hiçbir dallanma,
hiçbir özel durum kodu yok. Bu, zaman aşımı düzeninin en pratik yanı: bir işlevi zaman aşımına
duyarlı yapmak, o işlevin içine “eğer süre dolduysa” türünden bir kontrol eklemeyi
gerektirmiyor; yalnız bir select içine iptal dalını eklemek yetiyor, gerisini kapatma
sorumluluğu zaten kurulmuş olan bağlam kendisi hallediyor. Aynı dalSec, çağırana
WithCancelden gelen bir iptal ile de, WithTimeoutdan gelen bir zaman aşımıyla da, hiçbir
değişiklik gerekmeden hizmet ediyor.
Zaman Aşımı Bekleyeni Kurtarır, Beklenen İşi Durdurmaz
Yukarıdaki dalSec çağrısı döndüğünde, çağıran taraf kurtulmuş oluyor — artık bekleme
yok, bir sonuç ya da bir iptal metniyle devam edebiliyor. Ama bu kurtuluş, sonucu üretmesi
beklenen işin de durduğu anlamına gelmiyor. Aşağıdaki ölçüm bunu kurulum 2’nin üzerine inşa
ediyor: çağıran taraf iptal dalından çıktıktan sonra, aslında sonucu üretmekte olan bir iş
goroutine’i başlıyor ve sonucunu göndermeye çalışıyor — ama artık onu alacak kimse yok.
// zamanasimiSizinti.go — zaman asimi bekleyeni kurtarir ama beklenen isi durdurmaz
package main
import (
"context"
"fmt"
"runtime"
)
func dalSec(ctx context.Context, sonuc <-chan string) string {
select {
case deger := <-sonuc:
return "sonuc: " + deger
case <-ctx.Done():
return "iptal: " + ctx.Err().Error()
}
}
func main() {
ctx, iptalEt := context.WithCancel(context.Background())
iptalEt() // zaman asimini temsil eder: cagiran zaten vazgecmis durumda
sonuc := make(chan string) // tamponsuz, henuz hicbir gonderici yok
fmt.Println("-- cagiran dalindan cikiyor --")
fmt.Println("secilen dal:", dalSec(ctx, sonuc))
// Cagiran cikti. Is aslinda hala suruyormus gibi modellemek icin is goroutine'i
// simdi baslatiliyor ve sonucunu ayni kanala yazmaya calisiyor. Baglami hic
// denetlemedigi icin devam ediyor, ama artik alici yok.
taban := runtime.NumGoroutine()
go func() {
sonuc <- "gec kalan sonuc"
}()
fmt.Printf("is goroutine'i hala calisiyor mu (sizinti): %v\n", runtime.NumGoroutine() > taban)
}
-- cagiran dalindan cikiyor -- secilen dal: iptal: context canceled is goroutine'i hala calisiyor mu (sizinti): true
KL21. dalSec çağrıldığında sonuc kanalına henüz hiçbir goroutine yazmıyor — bu yüzden
seçilen dal kesin biçimde ctx.Done(). İş goroutine’i ancak dalSec döndükten sonra
başlıyor; bu sıralama bilinçli, çünkü amaç iş goroutine’inin select sırasında var olup
olmamasını bir zamanlama sorusu olmaktan çıkarmak. İş goroutine’i başladığında kendi sonucunu
sonuc kanalına göndermeye çalışıyor, ama kanal tamponsuz ve artık onu okuyan hiç kimse yok —
dalSec çoktan döndü, ana goroutine artık sonucu dinlemiyor. Gönderim kalıcı olarak
bloke oluyor.
KL22. runtime.NumGoroutine() > taban sonucu true — bu, önceki derslerde defalarca
görülen boolean sızıntı ölçütünün burada üçüncü kez, ama yeni bir sebeple ortaya çıkışı: işçi
havuzunda sebep kapatmanın unutulmasıydı, yaymada sorumluluğun hiç verilmemesiydi, boru
hattında işin hiç bitmemesiydi. Burada sebep tamamen farklı — kapatılacak bir kanal bile yok,
sorun iş goroutine’inin kimin dinlediğini hiç bilmemesi. dalSecin ctx.Done() dalını
seçmesi, iş goroutine’ine hiçbir şey söylemiyor; iş goroutine’i kendi payına düşen hesaplamayı
yapıp sonucu göndermeye çalışmaya devam ediyor, tıpkı önceki dersteki ihmalkâr goroutine’in
iptali hiç denetlemeden işini bitirmesi gibi.
tabanın bu ölçümde iş goroutine’i başlamadan hemen önce, ikinci bir kez yakalanması da
dikkat çekici: ilk iki kurulumun kendi goroutine’leri (varsa) zaten sonlanmış olabilir, ölçümün
yalnız üçüncü kurulumdaki tek goroutine’e odaklanması gerekiyor. Bu, önceki derslerdeki
runtime.NumGoroutine() kullanımlarıyla aynı disiplin: taban her zaman ölçülecek goroutine’in
başlatılmasından hemen önce alınıyor, böylece karşılaştırma yalnız o tek goroutine’in var olup
olmadığını yansıtıyor — programın başından beri çalışan başka goroutine’lerin sayısı
karışmıyor.
Bu, dersin başlığındaki iki sözcüğün (“zaman aşımı” ve “kaynak sızıntısı”) neden birlikte anıldığını açıklıyor. Zaman aşımı, çağıranı bekleme durumundan kurtarıyor — çağıran artık sonsuza kadar bloke değil, bir yanıtla (iptal ya da sonuç) devam edebiliyor. Ama zaman aşımının kurtardığı yalnız bekleyen taraf; sonucu üretmesi beklenen iş, zaman aşımından tamamen habersiz, kendi hızında çalışmaya devam ediyor. Çağıran “vazgeçtim” dediğinde iş goroutine’ine bu haber hiç ulaşmıyor — tıpkı önceki dersteki gibi, bağlam kimseyi zorla durdurmuyor, yalnız bekleyen tarafa bir çıkış kapısı açıyor.
Bu ölçümdeki iş goroutine’i kasıtlı olarak en yalın hâliyle kuruldu: tek bir gönderim, hiçbir hesaplama yok. Gerçek bir sistemde bu goroutine bir ağ isteği, bir veritabanı sorgusu ya da bir dosya okuması olurdu — kendi süresi olan, kesilmeden tamamlanmak isteyen bir iş. Zaman aşımı o işin kendisini durdurmuyor; yalnız çağıranın o işi beklemekten vazgeçmesini sağlıyor. İşin arkada gerçekten tamamlanıp tamamlanmadığı bu ölçümün konusu değil — burada kesin olan tek şey, işin sonucunu artık kimsenin almayacağı, çünkü tamponsuz kanalın tek okuyucusu zaten ayrılmış durumda.
Bunun pratik sonucu açık: bir zaman aşımı kurmak tek başına yeterli değil. İş goroutine’inin
kendisi de aynı bağlamı paylaşmalı ve kendi ctx.Done()unu denetlemeli — aksi hâlde
çağıran taraf “zaman aşımına uğradı” deyip devam ederken, arkada bırakılan iş sonsuza kadar
sürebiliyor. Bu ders bu ikinci adımı kurmuyor, yalnız kurulmadığında ne olduğunu ölçüyor;
iş goroutine’inin de aynı ctxi alıp kendi selectinde denetlemesi, önceki iki derste
görülen düzeneğin (bir aşamaya ctx.Done() dalı eklemek) doğrudan tekrarı olurdu.
Bu payın beş dersi, aynı sorunun beş ayrı yüzünü ölçtü ve hepsi aynı noktada buluşuyor: bir
goroutine, kendi ctx.Done()unu ya da kendi kapatılan kanalını denetlemediği sürece, ne
kadar iyi niyetli bir düzenek kurulursa kurulsun, dışarıdan gelen hiçbir sinyal onu durduramaz.
İşçi havuzunda bu denetim for is := range islerin kendisiydi; boru hattında her aşamanın
kendi girdi kanalıydı; burada ise iş goroutine’inin hiç yazmadığı, eksik kalan bir select
dalı. Beşi de aynı dersi farklı bir açıdan tekrar ediyor: durma, goroutine’in kendi kod
yolunda bir kontrol noktasına uğramasına bağlı, kontrol noktası yoksa dışarıdaki hiçbir sinyal
bir anlam taşımıyor.
Özet
- Zaman aşımı bir
selectdeyiminin bir dal seçimidir; süre hiç ölçülmez, yalnız hangi dalın seçildiği yazılır. - Sonuç önceden hazırsa ya da bağlam önceden iptal edilmişse,
selectin seçtiği dal belirlenimcidir — kurulumda yalnız bir dal gerçekten hazır olduğu için. - İptal edilmiş bir bağlamın
selectaçısından görünümü, süresi dolmuş bir zaman aşımı bağlamıyla aynıdır;selectiptalin sebebini bilmez, yalnızDone()in kapalı olup olmadığını görür. - Zaman aşımı bekleyen tarafı kurtarır ama sonucu üretmesi beklenen işi durdurmaz; iş goroutine’i kendi bağlamını denetlemediği sürece çalışmaya devam eder.
- Bu son sızıntı da bir boolean olarak ölçülür; sebebi önceki derslerdeki üç sebepten (unutulmuş kapatma, sahipsiz sorumluluk, bitmeyen iş) farklı — kimin dinlediğini bilmeyen bir gönderici.
context.WithTimeout(göreli süre) vecontext.WithDeadline(mutlak an),selectaçısındancontext.WithCancelden ayırt edilemez; üçü de kapanan birDone()kanalı verir.
Sonraki Adım
Bu payın beş dersi boyunca kurulan her düzenek — işçi havuzu, yayma ve toplama, boru hattı, bağlam, zaman aşımı — aynı ilkeye dayandı: goroutine’ler birbirine iletişimle haber veriyordu, bir kanal göndererek ya da bir kanalı kapatarak. Zaman aşımı bekleyeni kurtarır ama beklenen işi durdurmaz; sıradaki konu paylaşılan duruma dönüyor — iletişim yerine ortak bir değeri korumayı seçtiğinde ne değişiyor. Orada goroutine’ler artık birbirine kanal üzerinden haber vermiyor, aynı belleğe erişimlerini bir kilitle sınırlıyor — ve durma kararı, kilidi bırakan tarafın bir adımına bağlı kalmaya devam ediyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.