İçeriğe geç
academia.sh

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 select deyiminin 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 select açısından görünümü, süresi dolmuş bir zaman aşımı bağlamıyla aynıdır; select iptalin sebebini bilmez, yalnız Done()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) ve context.WithDeadline (mutlak an), select açısından context.WithCancelden ayırt edilemez; üçü de kapanan bir Done() 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat