İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat