İçeriğe geç
academia.sh

Ders 05 / 14

Kanal Kapatma ve Aralık

Bir kanalın kapatılması bir sonlanma sinyalidir: kapanan kanalda range biter, kapanan kanaldan doğrudan alım sıfır değer ve false döner. Dört alım biçiminin (açık, kapalı-dolu, kapalı-boş, tekrar kapalı-boş) yanıtı bunu ölçüyor. Kapatma sorumluluğu her zaman göndericidedir. Sınırlayıcı ölçüm: kapalı bir kanala gönderim de, bir kanalı ikinci kez kapatmak da panikler; ikisi de recover ile yakalanıp yalnız panik=true/false olarak yazılıyor.

İçindekiler

Önceki ders selectin belirlenimli seçimini ve rastgele istisnasını ölçtü, ama bir soruyu açık bıraktı: kapanmış bir kanal select içinde nasıl davranır? Bu ders bu soruyla açılıyor ve konunun son sorusuna geçiyor — bir kanalın kapatılması neyi bildiriyor, ve bu bildirim kimin sorumluluğunda. Bu, konunun beşinci ve son dersi: goroutine’lerin nasıl durduğu, kanalların ne aktardığı, tamponun neyi ertelediği ve selectin nasıl seçtiği ölçüldü; geriye tek bir soru kaldı — bir iletişim ne zaman biter, ve bu bitişi kim ilan eder.

Bir kanal close(kanal) ile kapatılabilir. Kapatma bir değer göndermez; kanalın artık hiçbir yeni değer taşımayacağını bildiren ayrı bir sinyaldir. Bu sinyal iki yerde görünür: doğrudan bir alımda (deger, tamam := <-kanal) ve bir range döngüsünde. Bu dersin ölçtüğü şey, bu iki görünümün tam olarak ne söylediği. close yalnızca kanallar üzerinde tanımlıdır; başka hiçbir tip için karşılığı yoktur, çünkü bir kanalın “artık boş kalacak” demenin gerçek bir karşılığı vardır, oysa bir tam sayının ya da bir dizginin böyle bir sonlanma kavramı hiç yoktur.

Kapatma, bu kursun ilk dersinde sayılan dört durma biçiminden birine doğrudan bağlanıyor: “beklediği geldi” sınıfının bir goroutine’i, beklediği şeyin bir kanal kapatması olduğunda, kapatma anında serbest kalıyor. O derste kapanan bir kanalda range yapan bir goroutine tam olarak bu yolla durmuştu; bu ders o tek satırlık gözlemi genişletiyor. Aynı dersteki “hiç durmadı” örneklerinden biri de kapatılmayan bir kanaldı — bu ders o örneğin neden hiç durmadığını da ayrıca ölçüyor.

Kapanan Kanal select İçinde Hemen Hazırdır

Önceki dersin sonunda bırakılan soru buydu: kapanmış bir kanal, hiç değer içermese bile, bir select dalını hazır kılar mı? Yanıt evet — kapanmış bir kanaldan alım hiçbir zaman engellemez, bu yüzden o dal her zaman seçilmeye hazırdır. Taşıdığı yanıt ise bir değer değil, kanalın bittiğinin haberidir.

// selectKapali.go — kapanan bir kanal select icinde hemen hazir olur, sifir deger ve false doner
package main

import "fmt"

func main() {
	kapali := make(chan int)
	close(kapali)

	bekleyen := make(chan int)

	select {
	case v, tamam := <-kapali:
		fmt.Println("kapali kanaldan gelen dal secildi, deger=", v, "tamam=", tamam)
	case v, tamam := <-bekleyen:
		fmt.Println("beklenen kanaldan gelen dal secildi, deger=", v, "tamam=", tamam)
	}
}
kapali kanaldan gelen dal secildi, deger= 0 tamam= false

GK18.bekleyen hiçbir zaman doldurulmuyor ve hiçbir zaman kapatılmıyor; bu dal sonsuza kadar hazır olmayacaktı. kapali ise close çağrısından itibaren her zaman hazırdır — bu yüzden select bu iki dal arasında hiç tereddüt etmeden kapali dalını seçiyor, önceki dersteki rastgelelik burada devreye girmiyor çünkü yalnız bir dal gerçekten hazır. v sıfır değerinde (0) kalıyor ve tamam false dönüyor — bu iki yanıtlı biçim, M09/K01 Go Temelleri kursunda sözlük erişiminde görülen aynı sözdiziminin kanallardaki karşılığı, ama taşıdığı anlam farklı: sözlükte tamam bir anahtarın var olup olmadığını söylerken, kanalda kanalın açık olup olmadığını söylüyor.

Bu, önceki dersteki nil kanal gözlemiyle de bağlantılı: bir select içinde bir dalı etkisiz kılmanın yolu o dalın kanalını nile çevirmekti, çünkü nil bir kanal hiçbir zaman hazır olmuyordu. Kapalı bir kanal bunun tam tersi uçta duruyor — hiçbir zaman hazır olmamak yerine, her zaman hazır. İkisi de birer sınır durum, ama zıt yönlerde: biri sonsuza kadar bekletir, öteki hiç beklemez.

Bu iki sınır durum birlikte kullanılabiliyor da: bir goroutine, bir işi bitirdiğinde kendi ilerleme kanalını nile çevirip o daldan artık hiç seçilmemesini sağlayabilir, ya da bir iptal kanalını kapatıp tüm bekleyenlerin aynı anda uyanmasını sağlayabilir. Bu kalıpların ikisi de B payının konusu; burada yalnız uçların kendisi ölçüldü.

Dört Alım Biçimi

Kapatmanın taşıdığı bilgi, bir kanalın o an hangi durumda olduğuna göre değişiyor. Dört durum birbirinden ayrılıyor: kanal açıkken bir değer alınıyor, kanal kapatıldıktan sonra tamponda kalan bir değer alınıyor, tampon da boşaldıktan sonra alınıyor, ve bu son durum tekrarlanıyor.

// dortAlim.go — kapali kanaldan alimin dort bicimi: acik, kapali-dolu, kapali-bos, araliktan cikma
package main

import "fmt"

func main() {
	kanal := make(chan int, 2)
	kanal <- 10
	kanal <- 20

	v1, ok1 := <-kanal
	fmt.Println("acikken alim           :", v1, ok1)

	close(kanal)

	v2, ok2 := <-kanal
	fmt.Println("kapali ama tamponda var :", v2, ok2)

	v3, ok3 := <-kanal
	fmt.Println("kapali ve bos           :", v3, ok3)

	v4, ok4 := <-kanal
	fmt.Println("tekrar kapali ve bos    :", v4, ok4)

	toplam, adimSayisi := 0, 0
	kanal2 := make(chan int, 3)
	kanal2 <- 1
	kanal2 <- 2
	kanal2 <- 3
	close(kanal2)
	for deger := range kanal2 {
		toplam += deger
		adimSayisi++
	}
	fmt.Println("range ile toplam ve adim:", toplam, adimSayisi)
}
acikken alim           : 10 true
kapali ama tamponda var : 20 true
kapali ve bos           : 0 false
tekrar kapali ve bos    : 0 false
range ile toplam ve adim: 6 3

GK19. — İlk alım kanal daha kapatılmadan önce yapılıyor ve olağan bir alım gibi true dönüyor. close(kanal) çağrıldığında tamponda hâlâ bir değer (20) duruyor; kapatma bu değeri silmiyor — ikinci alım onu true ile veriyor. Tampon boşaldıktan sonraki üçüncü alım artık sıfır değer (0) ve false veriyor; dördüncü alım da aynı yanıtı tekrarlıyor, çünkü kapalı ve boş bir kanaldan alım sonsuza kadar bu yanıtı verir, bir daha asla bloklanmaz. range ise bu davranışı bir döngüye çeviriyor: kanal kapandığında ve tamponu boşaldığında döngü kendiliğinden bitiyor, break yazmaya gerek kalmıyor — Denetim Akışı dersinde M09/K01’de görülen “yazılmayan break” kalıbının kanallardaki karşılığı.

Bu dört yanıt, kapatmanın neyi bildirdiğini tam olarak gösteriyor: kapatma var olan değerleri geçersiz kılmıyor, yalnızca yeni değer gelmeyeceğini bildiriyor. Tamponda bekleyen değerler kapatmadan sonra da normal şekilde alınabiliyor; sinyal yalnız tampon tükendiğinde devreye giriyor.

Bu dört durumun sırayla ölçülmüş olması önemli: ölçüm hiçbir goroutine başlatmadan, tek bir goroutine içinde, adım adım kuruluyor. Kapatma anında tamponda kaç değer kaldığı önceden biliniyor (kanal <- 10 ve kanal <- 20 iki değer koydu, yalnız biri alındı), bu yüzden “kapalı ama tamponda var” durumunun tam olarak bir kez görüneceği baştan kesin. Aynı ölçüm tamponsuz bir kanalda yapılsaydı ikinci durum (kapalı ama tamponda değer var) hiç gerçekleşmezdi — tamponsuz bir kanalda kapatma anında bekleyen bir değer olamaz, çünkü her gönderim ancak bir alıcı hazır olduğunda tamamlanıyordu.

Kapatma Sorumluluğu Göndericide Olur

Bir kanalı kapatma yetkisi teknik olarak hem gönderen hem alan tarafından çağrılabilir — close bir kanal değerini alan her taraf tarafından çalıştırılabilir. Ama bu yetkinin doğru kullanımı tektir: yalnızca gönderen taraf kapatabilir, çünkü yalnız gönderen tarafın “artık gönderecek değerim kalmadı” bilgisi vardır. Bir alıcının kanalı kapatması, göndericinin elinde hâlâ göndermeyi planladığı bir değer varken kanalı kapatmış olması riskini taşır — ve kapalı bir kanala gönderim panikler, bu dersin son bölümünün konusu.

Bu kural, kanalın kendi tipinde yazılı değil. chan<- int ile kısıtlanmış bir parametre bile close çağırabilir — yön kısıtlaması yalnızca gönderim ile alımı ayırıyordu, kapatma ikisinden de ayrı bir işlem ve her iki yönde de tanımlı. Kuralın kaynağı dil değil, bilgi: yalnızca göndericinin ne zaman göndermeyi bitireceğini bilme ihtimali var, alıcının böyle bir bilgisi hiçbir zaman olamaz. Bir alıcı, kendisine daha gönderim yapılıp yapılmayacağını kaynak koddan asla kesin biçimde bilemez.

Kapatmanın hiç yapılmamasının da bir bedeli var. Kapatılmayan bir kanalda bekleyen bir range, kanal hiçbir zaman kapatılmadığı sürece hiçbir zaman durmaz — M09/K03’ün ilk dersinde ölçülen “hiç durmadı” sınıfının burada tekrar görünen hâli. O derste bu sınıfa giren üç örnekten biri tam olarak buydu: kapanmayan bir kanalda range yapan bir goroutine, programın geri kalanı bitse bile kendisi hiç bitmiyordu.

// kapatilmayanRisk.go — kapatilmayan bir kanalda range hic durmaz, sizinti boolean olarak gorunur
package main

import (
	"fmt"
	"runtime"
)

func main() {
	taban := runtime.NumGoroutine()

	kapanmayan := make(chan int)
	basladi := make(chan struct{})
	go func() {
		close(basladi)
		for range kapanmayan {
		}
	}()
	<-basladi

	fmt.Println("taban sayidan fazla goroutine var mi:", runtime.NumGoroutine() > taban)
}
taban sayidan fazla goroutine var mi: true

GK20.basladi kanalı yalnızca goroutine’in başladığını doğrulamak için var; asıl ölçülen kapanmayan üzerindeki rangein hiç bitmediği. Bu goroutine, kapanmayan kapatılmadığı için, program sonlanana kadar bellekte kalıyor. runtime.NumGoroutinein verdiği sayı hiçbir yerde basılmıyor, yalnızca tabanla karşılaştırılıp true/falseya indirgeniyor — kursun ilk dersinde kurulan kural burada aynen sürüyor. Bu ölçüm, kapatmanın isteğe bağlı bir incelik değil, bekleyen taraf varsa bir zorunluluk olduğunu gösteriyor.

Bu ölçümün doğruluğu iki şeye dayanıyor. Birincisi, <-basladi alımı, goroutine’in en azından close(basladi) satırına ulaştığını garanti ediyor — bu da goroutine’in henüz dönmediğini, dolayısıyla hâlâ sayılabilir olduğunu gösteriyor. İkincisi, goroutine’in gövdesinde close(basladi)den sonra tek bir satır var (for range kapanmayan) ve bu satır, kapanmayan hiç kapatılmadığı için asla bitmiyor; goroutine’in <-basladi alımından sonra dönmüş olma ihtimali yok. Bu iki gözlem birlikte, runtime.NumGoroutinein her koşumda aynı yanıtı (true) vermesini garanti ediyor — burada da hiçbir sıra çizelgeleyiciye bırakılmadı.

Sınırlayıcı Ölçüm: Kapalı Kanala Gönderim ve İkinci Kapatma Panikler

Kapatmanın kendi kuralları var, ve bu kurallar ihlal edildiğinde dil sessiz kalmıyor. Kapalı bir kanala gönderim yapmaya çalışmak panikler; aynı kanalı ikinci kez kapatmaya çalışmak da panikler. Bu iki davranış, recover ile yakalanıp yalnızca panik=true/ panik=false olarak yazılıyor — yığıt izi ve hata metni basılmıyor.

// kapatmaSorumlulugu.go — kapali kanala gonderim ve iki kez kapatma panikler, recover ile yakalanir
package main

import "fmt"

func panikliyorMu(f func()) (p bool) {
	defer func() {
		if recover() != nil {
			p = true
		}
	}()
	f()
	return false
}

func main() {
	kanal := make(chan int, 1)
	close(kanal)

	gonderimPanikledi := panikliyorMu(func() { kanal <- 1 })
	fmt.Println("kapali kanala gonderim panik=", gonderimPanikledi)

	ikinciKapatmaPanikledi := panikliyorMu(func() { close(kanal) })
	fmt.Println("kanali ikinci kez kapatmak panik=", ikinciKapatmaPanikledi)
}
kapali kanala gonderim panik= true
kanali ikinci kez kapatmak panik= true

GK21.panikliyorMu, çağırdığı işlevi bir defer/recover çifti içinde çalıştırıyor ve yalnız panikleyip panikelemediğini bir bool olarak döndürüyor — M09/K01 Go Temelleri kursunda kurulan panik/kurtarma kalıbının aynısı. defer içindeki işlev recover()ı çağırıyor; bir panik varsa recover onu durdurup değerini döndürüyor ve p değişkenini true yapıyor, panik yoksa recover nil dönüyor ve p adlandırılmış dönüş değeri olarak baştaki falseunda kalıyor. kanal bir değerle dolu bile olsa (kapasitesi 1, ama içi boş burada) kapatıldıktan sonra her gönderim panikliyor; bu, üçüncü bölümdeki “kapatma değerleri silmez” gözlemiyle çelişmiyor — kapatma yalnızca yeni göndermeyi yasaklıyor, var olanı okumayı değil. İkinci kapatma da aynı şekilde panikliyor, çünkü close bir kanalın durumunu geri döndürülemez biçimde değiştiriyor; kapalı bir kanalı “tekrar kapatmak” diye bir işlem dilde tanımlı değil.

Bu iki panik birlikte, kapatma sorumluluğunun neden tek bir tarafta toplanması gerektiğini gösteriyor. İki ayrı goroutine aynı kanalı kapatmaya çalışırsa, ikincisi panikler. İki ayrı goroutine aynı kanala gönderim yapıyorsa ve biri kanalı kapatırsa, ötekinin bir sonraki gönderimi panikler. Kapatmanın kimin işi olduğu konusunda bir anlaşma olmadan birden çok goroutine aynı kanalı paylaşamıyor — bu sorunun kendisi B payının ilk konusu.

Bu iki panik arasında da bir fark var. Kapalı kanala gönderim panikleri, kaynak kodun her koşumda aynı yerde, aynı koşullarda tetiklenir — closedan sonra bir gönderim varsa panik kesindir. İkinci kapatma panikleri ise genellikle iki ayrı goroutine’in aynı kanalı kapatmaya çalıştığı bir yarış durumunun sonucudur; hangi goroutine’in önce kapattığı belirlenimli olmayabilir, ama en az birinin panikleyeceği kesindir. Bu ders ikinci durumu da tek bir goroutine içinde, art arda iki close çağrısıyla, belirlenimci biçimde kuruyor — gerçek bir yarış koşturmadan aynı sonucu gösteriyor.

Panik burada bir hata gösterimi değil, bir sözleşme ihlali gösterimi. Kapalı bir kanala gönderim ya da bir kanalı ikinci kez kapatmak, çalışma zamanı hatası değil, programın kendi mantığının bir kanalın durumunu yanlış izlediğinin kanıtıdır. Bu ikisi recover ile yakalanabiliyor olsa da, kursun kuralı hâlâ geçerli: panik metnini basmak yerine yalnızca true/false yazılıyor, çünkü panik metni sürüme bağlı bir ayrıntı.

Özet

  • Kapanmış bir kanal select içinde her zaman hazırdır; taşıdığı yanıt sıfır değer ve false, bir veri değil bir sonlanma haberidir.
  • Kapatma var olan değerleri silmez: tamponda bekleyen değerler kapatmadan sonra da normal şekilde alınabilir, yalnız tampon tükendiğinde sıfır değer ve false başlar.
  • range, kanal kapanıp tamponu boşaldığında kendiliğinden durur; ayrıca bir break yazmaya gerek yoktur.
  • Kapatma sorumluluğu göndericidedir; kapatılmayan bir kanalda bekleyen bir range hiçbir zaman durmaz ve bu, ölçülebilir bir goroutine sızıntısıdır.
  • Kapalı bir kanala gönderim ve bir kanalı ikinci kez kapatmak panikler; ikisi de geri döndürülemez bir durum değişikliğinin ihlalidir.
  • Kapatma yetkisi kanalın tipinde değil bilgide oturur: yalnızca göndericinin ne zaman göndermeyi bitireceğini bilme ihtimali vardır, alıcının hiçbir zaman yoktur.

Sonraki Adım

Bu konunun beş dersi tek bir goroutine ile tek bir goroutine arasındaki iletişimi ölçtü: bir kanal, iki taraf, bir gönderici, bir alıcı. Her ders bu ikili ilişkinin bir yönünü ölçtü — goroutine’in kendisi, kanalın iki işi, tamponun ertelemesi, selectin seçimi, kapatmanın sinyali — ve her biri kursun açılışındaki soruya, bir goroutine’in nasıl duracağına, bir parça yanıt ekledi.

Kapatma bir sonlanma sinyalidir ve sorumluluğu göndericidedir — ama gönderici tek değilse, ya da alıcı tek değilse, bu sorumluluk nasıl paylaşılır? Bir kanalı birden çok goroutine aynı anda kapatmaya çalışırsa ikincisi panikler; bu dersin son ölçümü bunu gösterdi. Sıradaki konu bu sinyali çok sayıda goroutine arasında kullanan kalıplara bakıyor: işçi havuzları, yayma ve toplama, boru hatları — ve her birinde, kapatma sorumluluğunun kaç taraf arasında nasıl paylaşıldığı.

İ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