Ders 09 / 14
Bağlam Paketi
Tek bir iptal, ctx.Done() üzerinde bekleyen dört goroutine'in dördüne de aynı anda ulaşıyor; hepsi 'iptal' sınıfında duruyor ve ctx.Err() aynı sabit sebebi taşıyor. İstek kapsamlı bir değer (context.WithValue) taşımanın ne için olduğu ve ne için olmadığı ayrı ayrı ölçülüyor. Sınırlayıcı ölçüm: bağlam kimseyi zorla durdurmuyor, yalnız haber veriyor — ctx.Done()'u hiç denetlemeyen bir goroutine, bağlam zaten iptal edilmiş olsa bile kendi işini sonuna kadar tamamlıyor.
İçindekiler
Önceki ders bir sızıntıyı ölçüp çözümsüz bıraktı: bir boru hattında tüketici erken çıktığında,
önündeki aşamaların bunu bilecek hiçbir yolu yoktu. Aşamaların kendi girdi kanallarını
dinlemesi yetmiyordu — dışarıdan gelen, bütün aşamalara aynı anda ulaşan bir sinyale
ihtiyaç vardı. Bu ders o sinyali taşıyan aracı ölçüyor: bağlam paketi (context paketi). Bir
bağlamın iptal edilmesi, onu bekleyen kaç goroutine olursa olsun, hepsine tek bir olayla
ulaşıyor — kanaldan farklı olarak burada “kaç alıcı” sorusu hiç yok, çünkü bir bağlamın iptal
sinyali yayın gibi çalışıyor, tek bir alıcıya değil hepsine birden.
Bağlam paketinin ortaya çıkardığı context.Context tipi kendi başına yeni bir düzenek değil —
altında yatan yalnız bir kanal (Done()in döndürdüğü) ve bir hata değeri (Err()in
döndürdüğü). Yeni olan, bu ikilinin bir ağaç biçiminde türetilebilmesi: bir bağlamdan yeni
bir bağlam türetildiğinde (context.WithCancel, context.WithTimeout gibi çağrılarla),
üstteki bağlam iptal edildiğinde bütün alt bağlamlar da otomatik olarak iptal ediliyor. Bu ders
tek katmanlı bir ağaçla (context.Background()den türeyen tek bir bağlam) başlıyor; ağacın
kendisi bu dersin konusu değil, ama tek bir düğümün iptalinin kaç bekleyene ulaştığı burada net
biçimde ölçülüyor.
Tek Bir İptal, Zincirdeki Bütün Bekleyenler
Aşağıdaki ölçüm dört goroutine başlatıyor; her biri ctx.Done() üzerinde bekliyor. Tek bir
iptalEt() çağrısından sonra dördü de raporunu gönderiyor — hepsi aynı sınıfta: iptal.
// iptalyayilir.go — tek bir iptal, baglami bekleyen butun goroutine'lere ayni anda ulasir
package main
import (
"context"
"fmt"
"sort"
"sync"
)
type rapor struct {
id int
sebep string
}
func bekleyen(id int, ctx context.Context, raporlar chan<- rapor, grup *sync.WaitGroup) {
defer grup.Done()
<-ctx.Done()
raporlar <- rapor{id: id, sebep: "iptal"}
}
func main() {
const bekleyenSayisi = 4
ctx, iptalEt := context.WithCancel(context.Background())
raporlar := make(chan rapor, bekleyenSayisi)
var grup sync.WaitGroup
for id := 1; id <= bekleyenSayisi; id++ {
grup.Add(1)
go bekleyen(id, ctx, raporlar, &grup)
}
iptalEt()
grup.Wait()
close(raporlar)
kayitlar := make([]rapor, 0, bekleyenSayisi)
for r := range raporlar {
kayitlar = append(kayitlar, r)
}
sort.Slice(kayitlar, func(i, j int) bool { return kayitlar[i].id < kayitlar[j].id })
fmt.Println("-- tek iptal, dort bekleyen --")
for _, r := range kayitlar {
fmt.Printf("bekleyen %d sebep=%s\n", r.id, r.sebep)
}
fmt.Println("ctx.Err():", ctx.Err())
}
-- tek iptal, dort bekleyen -- bekleyen 1 sebep=iptal bekleyen 2 sebep=iptal bekleyen 3 sebep=iptal bekleyen 4 sebep=iptal ctx.Err(): context canceled
KL15. context.WithCancelin döndürdüğü ctx.Done() bir kanal, ama sıradan bir kanal
değil: iptalEt() çağrıldığında bu kanal kapanıyor, ve önceki derslerde defalarca görülen
“kapanan kanaldan alım bloklanmaz” davranışı burada da geçerli. Dört goroutine’in dördü de
<-ctx.Done() satırında bekliyordu; kanal kapandığı an dördü de aynı anda serbest kalıyor.
Kapatma önceki derslerde hep tek bir göndericiyle (bir üretici, bir aşama) ilişkiliydi;
burada da öyle — iptalEtin kendisi tek bir kapatma çağrısını sarmalıyor, birden fazla
iptalEt çağrısı bile kanalı yalnız bir kez kapatıyor, önceki dersteki “kanal kapatma atomik
bir işlemdir” gözleminin bağlam paketindeki karşılığı. Bu da bilinçli bir tasarım kararı:
iptalEti birden fazla kez, hatta birden fazla goroutine’den çağırmak güvenli — çağıranın
“acaba daha önce iptal ettim mi” diye kendi durumunu takip etmesi gerekmiyor, çünkü ikinci ve
sonraki çağrılar sessizce hiçbir şey yapmıyor.
Dört raporun idye göre sıralanması (sort.Slice), önceki derslerdeki toplama kanallarında
görülen aynı ihtiyaçtan geliyor: dört goroutine raporlar kanalına aynı anda yazabiliyor,
kanal kapandığında hangisinin önce ulaştığı zamanlamaya bağlı. Fark, bu kez sıralamanın
sonucu etkilememesi — dördünün de sınıfı zaten aynı (iptal), sıralama yalnız çıktının
okunabilir ve tekrarlanabilir olmasını sağlıyor.
KL16. Dördünün de aynı sınıfta (iptal) durması, önceki derste kurulan dört sınıflı
ölçütün (kendi, beklediği geldi, iptal, hiç durmadı) bu bağlamdaki en yalın hâli: hiçbiri kendi
gövdesini bitirmedi, hiçbiri başka bir goroutine’in bir adımını bekliyordu, dördü de tek bir
iptal olayının doğrudan sonucu olarak durdu. ctx.Err()in döndürdüğü metin de sabit —
“context canceled” — dilin kendi standart kütüphanesinin tanımladığı, sürüme bağlı olmayan bir
sabit dize; hangi bağlamın hangi sebeple iptal edildiğini ayırt etmek isteyen bir çağıran bu
metni okuyabiliyor.
Bu dördü, önceki derslerdeki “iptal” sınıfının tek örneğiydi (ortak tanımın iki bekleyeni)
— burada aynı sınıf dört kata çıkarılıyor ve sonuç değişmiyor, çünkü kanalın kapanması kaç
alıcı olursa olsun aynı davranışı gösteriyor. Bu ölçek bağımsızlığı bağlam paketinin asıl
gücü: bir web sunucusunda tek bir isteğin iptali, o isteği işleyen onlarca goroutine’e (bir
veritabanı sorgusu, birkaç dış çağrı, bir önbellek okuması) aynı ctx.Done() kanalı
üzerinden, hiçbirini tek tek uyarmaya gerek kalmadan ulaşabiliyor.
İstek Kapsamlı Değer Taşımak
Bağlam paketinin ikinci işi iptal yaymak değil, bir istek kapsamlı değeri çağrı zinciri
boyunca taşımak — örneğin bir istek kimliğini, zincirdeki her işlevin imzasını değiştirmeden,
en derindeki çağrıya kadar ulaştırmak. Bu, önceki derslerdeki hiçbir kanal düzeneğiyle
karşılanmayan ayrı bir ihtiyaç: kanallar veri akıtır, bağlam değerleri ise çağrı zincirinin
yanında taşır. İşçi havuzu, yayma/toplama ve boru hattı ölçümlerinin üçünde de veri hep
açık bir parametre ya da açık bir kanal üzerinden aktı; context.WithValuein çözdüğü sorun
farklı — bir istek kimliğini, o isteği işleyen zincirdeki her işlevin imzasına ayrı ayrı
eklemeden, zincirin en derinine kadar ulaştırmak.
Değer taşıma, iptal yaymadan bağımsız çalışıyor: aşağıdaki ölçüm önce bir değer taşıma kurulumunu, sonra (aynı programda, ayrı bir bağlamla) iptalin kimseyi durdurmadığını gösteren sınırlayıcı ölçümü kuruyor.
// degerVeIhmal.go — istek kapsamli deger tasima ve iptali hic denetlemeyen goroutine
package main
import (
"context"
"fmt"
)
type anahtarTipi string
const istekAnahtari anahtarTipi = "istek-kimligi"
func iclerdeOku(ctx context.Context) string {
deger, varMi := ctx.Value(istekAnahtari).(string)
if !varMi {
return "yok"
}
return deger
}
func main() {
ctx := context.WithValue(context.Background(), istekAnahtari, "istek-42")
fmt.Println("iclerdeki islevin okudugu istek kimligi:", iclerdeOku(ctx))
ctx2, iptalEt := context.WithCancel(context.Background())
iptalEt() // bu goroutine baslamadan once iptal zaten gerceklesti
adimlar := make(chan int, 3)
go func() {
// ctx2.Done() hic denetlenmiyor; bu goroutine iptali hic bilmiyor.
for adim := 1; adim <= 3; adim++ {
adimlar <- adim
}
close(adimlar)
}()
tamamlananAdim := 0
for range adimlar {
tamamlananAdim++
}
fmt.Println("goroutine calisirken baglam zaten iptal edilmis miydi:", ctx2.Err() != nil)
fmt.Printf("tamamlanan adim sayisi: %d / 3\n", tamamlananAdim)
}
iclerdeki islevin okudugu istek kimligi: istek-42 goroutine calisirken baglam zaten iptal edilmis miydi: true tamamlanan adim sayisi: 3 / 3
KL17. istekAnahtari, sıradan bir dizgi değil, kendi tanımlı anahtarTipi türünde bir
sabit — bu, bir başka paketin aynı dizgi değerini kendi anahtarı olarak kullanıp yanlışlıkla
aynı bağlam yuvasına yazmasını derleme zamanında engelliyor. iclerdeOku, ctx.Valuein
döndürdüğü değeri tip iddiasıyla (.(string)) okuyor ve ikinci dönüş değeriyle (varMi)
değerin hiç bulunmama ihtimalini ayırıyor — sözlükteki iki yanıtlı erişimin (M09/K01’den)
bağlam değerlerindeki karşılığı.
Bu taşıma biçiminin ne olmadığı da ölçünün bir parçası: context.WithValue, bir işlevin
ihtiyaç duyduğu gerekli bir bağımlılığı ya da parametreyi taşımak için değil, bir çağrı
zincirinin her katmanından geçmesi gereken, isteğe özgü ve o katmanların çoğunun
kullanmadığı üstbilgi türünden veriler (bir istek kimliği, bir izleme belirteci) için var.
iclerdeOku işlevi, istek kimliğini bulamazsa (varMi false ise) çökmüyor, yalnız “yok”
döndürüyor — çünkü bağlam değeri isteğe bağlı kabul ediliyor. Bir işlevin çalışması için
zorunlu olan bir değer (bir veritabanı bağlantısı, bir yapılandırma) bağlam üzerinden değil,
doğrudan bir parametre olarak geçirilmeli; aksi hâlde işlevin imzası neye ihtiyaç duyduğunu
söylemekten vazgeçmiş olur.
Bu ayrımın pratik sonucu, iclerdeOku gibi bir işlevin imzasına bakarak görülemiyor:
func iclerdeOku(ctx context.Context) string imzası, işlevin bir istek kimliğine ihtiyaç
duyduğunu hiç söylemiyor — yalnız bir bağlam alıyor, ve bağlamın içinde ne olduğu çağrı
zamanına kadar belirsiz kalıyor. Bu, önceki dersteki nil sözlük ve nil işaretçi
davranışlarına benzer bir açıklık: tip sistemi, bağlamın hangi anahtarları taşıdığını
denetlemiyor, yalnız bir anahtar sorulduğunda nil/varMi=false ile yokluğu bildiriyor.
Bir işlevin hangi bağlam anahtarlarını okuduğunu bilmenin tek yolu kaynağı okumak — imza bu
konuda hiçbir söz vermiyor.
Bağlam Kimseyi Durdurmaz, Yalnız Haber Verir
İkinci ölçüm bunu doğrudan kanıtlıyor. ctx2 goroutine başlamadan önce zaten iptal edilmiş
— iptalEt() çağrısı, go func() {...}() satırından önce çalışıyor. Bu sıralama bilinçli:
ölçümün amacı, “iptal henüz gerçekleşmemişken goroutine başladı, sonra iptal yetişemedi” gibi
bir zamanlama tartışmasını tamamen ortadan kaldırmak. Bağlam, goroutine daha var olmadan
zaten iptal edilmiş durumda; buna rağmen başlatılan goroutine ctx2.Done()u hiç denetlemiyor,
yalnız üç adımı sırayla üretip kanalını kapatıyor.
KL18. Sonuç net: ctx2.Err() != nil true — goroutine çalışmaya başladığında bağlam
zaten iptal edilmişti — ama tamamlanan adim sayisi yine de 3 / 3. Goroutine, bağlamın
iptal edilip edilmediğine hiç bakmadığı için, iptalin kendisinden tamamen habersiz
çalışmaya devam etti ve işini eksiksiz bitirdi. Bu, önceki dersin kapanışında bırakılan sorunun
tam yanıtı: bağlam paketi kimseyi zorla durdurmuyor, bir goroutine’i kesip atan bir
mekanizma değil. Yaptığı tek şey, iptal olduğunda bunu isteyen herkese haber vermek —
ctx.Done()u dinleyenler bu haberi alıp kendi kararlarını veriyor, dinlemeyenler hiçbir şey
fark etmiyor.
Bu davranış ilk bakışta bir kusur gibi görünebilir — neden bağlam kendisi bir goroutine’i
durdurmuyor? Yanıt Go’nun genel tasarımında yatıyor: dilin çalışma zamanı, bir goroutine’i
dışarıdan zorla durdurma aracı hiç sunmuyor (bir iş parçacığını dışarıdan kesmeye benzer
bir çağrı Go’da yok). Bir goroutine yalnız kendi isteğiyle, kendi kod yolunda bir returne ya
da fonksiyonun sonuna ulaşarak durabiliyor. Bağlam paketi bu kısıtın içinde çalışıyor: zorla
durdurma sunmadığı için, elindeki tek araç bilgilendirme — durması gerektiğini düşünen
goroutine’e bunu söylemek, kararı ona bırakmak. Bu, önceki derslerde defalarca görülen “durma
kararı her zaman goroutine’in kendi gövdesinde bir kontrol noktasına bağlıdır” ilkesinin en
açık örneği: kontrol noktası burada select içindeki bir case <-ctx.Done() dalı, ve o dal
kaynakta hiç yazılmamışsa iptalin kendisi de bir anlam ifade etmiyor.
Bu, önceki derste ölçülen boru hattı sızıntısının çözümünün de sınırını çiziyor: bir aşamaya
ctx.Done()u denetleyen bir select dalı eklemek, o aşamayı iptali fark eden hâle
getiriyor — ama bu ekleme her aşamada ayrı ayrı yapılmak zorunda. Örneğin kareAl aşaması,
kendi çıktısına düz bir gönderim yerine iki dallı bir select yazsaydı — biri değeri gönderen,
öbürü ctx.Done()u bekleyen — tüketici erken çıktığında bağlamı iptal ederek bu aşamayı da
serbest bırakabilirdi. Ama bu değişikliği zincirdeki her aşama ayrı ayrı yapmak zorunda;
bağlamın kendisi hiçbir goroutine’i otomatik olarak durdurmuyor, yayılan şey yalnız bilgi,
bu bilgiyi eyleme çevirmek her goroutine’in kendi sorumluluğunda kalıyor.
İptal bağlamının bu doğası, bir context.Context değerinin neden bir kilit ya da bir kesme
(interrupt) gibi çalışmadığını açıklıyor: o yalnız kapanan bir kanal, ve kapanan bir kanalı
kim dinliyorsa yalnız o tepki veriyor. Bu da bağlam paketini önceki derslerdeki bütün
düzeneklerle aynı temele oturtuyor — kanal, bekleme grubu, select, hepsi aynı ilkeye
dayanıyor: eşzamanlılık Go’da bir goroutine’i dışarıdan yönetmekle değil, goroutine’lerin
birbirine haber verdiği bir kanal ağıyla kuruluyor.
Özet
- Bir bağlamın iptal edilmesi,
ctx.Done()üzerinde bekleyen kaç goroutine olursa olsun, hepsine aynı anda ulaşıyor; hepsi aynı sınıfta (iptal) duruyor. ctx.Err(), dilin standart kütüphanesinin tanımladığı sabit bir metin döndürüyor; hangi bağlamın iptal edildiğini ayırt etmek için okunabiliyor.context.WithValue, isteğe bağlı, katmanlar arasında taşınan üstbilgi türünden veriler içindir; bir işlevin zorunlu bağımlılıklarının yerini tutmaz.- Bağlam kimseyi zorla durdurmuyor, yalnız haber veriyor —
ctx.Done()u denetlemeyen bir goroutine, bağlam iptal edilmiş olsa bile işini sonuna kadar sürdürüyor. - Bir goroutine’i iptale duyarlı hâle getirmek, o goroutine’in kendi döngüsüne bir
ctx.Done()denetimi eklemesini gerektiriyor; bu otomatik gerçekleşmiyor. - Go’nun çalışma zamanı bir goroutine’i dışarıdan zorla durdurma aracı sunmuyor; bağlam bu kısıt içinde çalışıyor ve elindeki tek araç bilgilendirme, zorlama değil.
Sonraki Adım
Bu derste bağlamın kendisinin ne zaman iptal edildiği önceden biliniyordu — iptalEt()
açıkça çağrıldı, bekleyenler bu çağrıyı bekledi. Ama bir bağlamın iptal edilmesinin en yaygın
sebebi, birinin bilinçli bir karar vermesi değil, bir sürenin dolması: bir ağ çağrısının,
bir veritabanı sorgusunun makul bir süre içinde yanıt vermesini beklemek, vermezse vazgeçmek.
Sıradaki ders bu özel durumu ölçüyor: bir select deyimi bir sonucu mu yoksa bir iptali mi
aldığına göre bir dal seçtiğinde, zaman aşımı da bu seçimin bir biçimi oluyor — ölçülen şey
geçen süre değil, hangi dalın seçildiği; bu ders zaten bilinen bir iptali ölçtü, sıradaki ders
henüz gerçekleşmemiş bir iptali (bir zaman aşımını) aynı select düzeneğiyle ölçüyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.