İçeriğe geç
academia.sh

Ders 03 / 14

Tamponlu ve Tamponsuz Kanallar

Bir kanalın kapasitesi, engellemenin nerede başladığını değiştirir: kapasite dolana kadar gönderim hiç beklemeden geçer, dolduğunda tamponsuz kanaldaki gibi engeller. Üç kapasitede beş deneme gönderiminin kaçının engellenmeden geçtiği ölçülüyor. Sınırlayıcı ölçüm: tampon eşzamanlamayı kaldırmıyor, yalnız erteliyor — tamponlu bir kanala gönderim tamamlandığında gönderen, alıcının o değeri aldığını hâlâ bilmiyor.

İçindekiler

Önceki ders tamponsuz bir kanalın iki işi birden yaptığını ölçtü: bir değer taşımak ve göndereni ile alıcıyı aynı noktada buluşturmak. Bir kanala kapasite eklendiğinde bu iki iş ortadan kalkmıyor — yalnız aralarındaki bağ gevşiyor. Bu ders tamponun engelleme davranışını nerede değiştirdiğini ölçüyor ve neyi değiştirmediğini ayrı bir bölümde sınırlıyor.

Bir kanala kapasite vermek, kanalı kendi içinde küçük bir dizi tutan bir yapıya dönüştürür. Bu dizinin varlığı gönderen tarafa bir tür esneklik verir: alıcı henüz hazır olmasa bile, tampon boş yer taşıdığı sürece gönderim beklemeden ilerleyebilir. Ama bu esnekliğin bir sınırı var, ve o sınır tam olarak kapasitenin kendisi. Bu ders bu sınırı ve sınırın ne zaman aşıldığını ölçüyor.

Kapasite, kanal oluşturulurken bir kez belirlenir ve sonradan büyütülüp küçültülemez — make(chan int, 3) ile açılan bir kanal, ömrü boyunca en fazla üç değer tutabilir. Bu sabitlik, kapasitenin bir çalışma zamanı ayarı değil, bir tasarım kararı olduğunu gösterir: kod yazan kişi, göndericinin alıcıyı ne kadar “öne geçebileceğine” baştan karar verir. Bu kararın doğru büyüklüğü tek bir kurala bağlanmaz — çok küçük bir kapasite göndericiyi gereğinden sık bekletir, çok büyük bir kapasite ise alıcının ne kadar geride kaldığını gizler; bu ders büyüklüğün nasıl seçileceğini değil, seçilen büyüklüğün ne yaptığını ölçüyor.

Kapasite, Engellemenin Başladığı Noktadır

make(chan T, n) çağrısındaki ikinci argüman kanalın kapasitesidir; verilmezse (make(chan T)) kapasite sıfırdır ve önceki dersin tamponsuz kanalı ortaya çıkar. Tamponsuz kanal ayrı bir tip değildir — kapasitesi sıfır olan bir kanaldır, ve sıfır kapasite “tampon her zaman dolu” ile aynı sonucu verir: her gönderim, kapasitenin dolu olduğu bir kanaldaki gibi, doğrudan bir alıcı bekler.

Bu tek fark, iki kanal kullanımının aynı sözdizimle yazılabilmesini sağlıyor: bir fonksiyon chan int parametresi aldığında, çağıranın tamponlu mu tamponsuz mu bir kanal geçirdiğini kod içinden ayırt etmek gerekmiyor — ikisi de aynı gönderim ve alım sözdizimini kullanıyor, tek fark ne zaman bekleyeceği.

Kapasitesi n olan bir kanalda ilk n gönderim, hiçbir alıcı olmadan bile hemen tamamlanır; kanal bu değerleri kendi içinde tutar. n+1. gönderim ise tamponu doldurmuş demektir ve tıpkı tamponsuz bir kanalda olduğu gibi bir alıcı gelene kadar bekler. Bu sınır kanalın kendi türünde değil, make çağrısına verilen ikinci argümanda taşınıyor; aynı chan int tipi kapasite 0 ile de, kapasite 1000 ile de oluşturulabiliyor ve iki kullanım da kaynak kodda aynı görünüyor — fark yalnızca make satırında.

Bunu üç ayrı kapasitede, her defasında beş deneme gönderimiyle, select+default kullanarak ölçmek mümkündür — hiç goroutine başlatmadan, çünkü hiçbir alıcı olmaması ölçümün bir parçası.

// kapasite.go — tampon kapasitesi dolana kadar gonderim engellenmeden geciyor, dolunca deneme gecmiyor
package main

import "fmt"

func kacTaneGeciyor(kapasite, denemeSayisi int) int {
	kanal := make(chan int, kapasite)
	gecen := 0
	for i := 0; i < denemeSayisi; i++ {
		select {
		case kanal <- i:
			gecen++
		default:
		}
	}
	return gecen
}

func main() {
	for _, kapasite := range []int{0, 2, 4} {
		fmt.Printf("kapasite=%d denemeSayisi=5 gecen=%d\n", kapasite, kacTaneGeciyor(kapasite, 5))
	}
}
kapasite=0 denemeSayisi=5 gecen=0
kapasite=2 denemeSayisi=5 gecen=2
kapasite=4 denemeSayisi=5 gecen=4

GK9. — Hiçbir alıcı hiçbir zaman başlatılmadı; bu yüzden sonuç kesinlikle belirlenimlidir ve rastgele bir zamanlamaya bağlı değildir — burada hiçbir goroutine yok, dolayısıyla ölçülen şey zamanlama değil, kanalın kendi kapasite kuralı. Kapasite 0 iken hiçbir gönderim geçmiyor — önceki dersteki “alıcı hiç yokken deneme hiçbir zaman geçmez” ölçümünün burada tekrar görünen hâli. Kapasite 2 iken tam iki, kapasite 4 iken tam dört deneme geçiyor: gecen sayısı her durumda tam olarak kapasiteye eşit, beşinci denemeye hiç ulaşmadan. Beş deneme yerine elli deneme yapılsaydı sonuç değişmezdi — tampon dolduktan sonraki her deneme, alıcı gelene kadar aynı şekilde default dalına düşer.

Bu, tamponun ne yaptığını tam olarak gösteriyor: engellemeyi ortadan kaldırmıyor, engellemenin başladığı noktayı kapasite kadar erteliyor. Sıfır kapasitede erteleme hiç yok; kapasite arttıkça gönderen taraf, hiçbir alıcı olmadan yapabileceği gönderim sayısını da artırıyor.

Bu erteleme, göndericinin alıcıdan bağımsız çalışabildiği pencerenin büyüklüğüdür. Kapasite 0 iken bu pencere yoktur, gönderen her adımda alıcıyı beklemek zorundadır. Kapasite büyüdükçe gönderen, alıcının hızına bakmadan art arda o kadar adım atabilir — ama pencere kapandığında (kacTaneGeciyor ölçümünde beşinci denemeden itibaren) davranış yeniden tamponsuz kanaldakiyle aynıya döner.

len ve cap Tamponun Anlık Durumunu Gösterir

Bir kanalın o anki doluluğu programın içinden de okunabilir: yerleşik len işlevi tamponda bekleyen, henüz alınmamış değer sayısını döndürür; cap işlevi ise kanalın sabit kapasitesini. İkisi birbirinden bağımsız bilgi taşır — cap bir kanal oluşturulduğu andan itibaren hiç değişmez, len her gönderim ve alımda değişir.

// lencap.go — kanalin len'i o an tamponda bekleyen deger sayisini, cap'i kapasiteyi verir
package main

import "fmt"

func main() {
	kanal := make(chan int, 3)
	fmt.Println("bos        : len=", len(kanal), "cap=", cap(kanal))

	kanal <- 1
	kanal <- 2
	fmt.Println("iki dolu   : len=", len(kanal), "cap=", cap(kanal))

	<-kanal
	fmt.Println("biri alindi: len=", len(kanal), "cap=", cap(kanal))
}
bos        : len= 0 cap= 3
iki dolu   : len= 2 cap= 3
biri alindi: len= 1 cap= 3

GK10. — Kanal boşken len sıfır, iki gönderimden sonra iki, bir alımdan sonra bir oluyor; cap üçü hiç değişmeden koruyor, çünkü kapasite make anında sabitleniyor ve sonradan değiştirilemiyor. len(kanal) == cap(kanal) olduğunda kanal doludur ve bir sonraki gönderim (bu ders boyunca ölçülen tam olarak bu nokta) beklemeden geçemez; len(kanal) == 0 olduğunda kanal boştur ve bir alım aynı şekilde bekler. Bu iki işlev, önceki bölümdeki kacTaneGeciyor ölçümünün içeriden bakan hâli: orada dışarıdan deneme sayısı sayılıyordu, burada kanalın kendi durumu doğrudan okunuyor.

Bu işlevlerin bir sınırı var: len yalnızca o an okunduğu andaki değeri verir, ve birden çok goroutine aynı kanala aynı anda erişiyorsa bu değer okunduğu anda bile değişmiş olabilir. Tek goroutine’in kullandığı bir kanalda len güvenilir bir gözlemdir; paylaşılan bir kanalda yalnızca bir anlık fotoğraftır, karar vermek için kullanılmaz. Örneğin len(kanal) < cap(kanal) doğruyken bir gönderim yapmaya karar vermek güvenli değildir: bu okuma ile gönderimin kendisi arasında başka bir goroutine aynı kanala yazabilir ve tampon okuma anından sonra dolabilir; bu durumda gönderim yine de bekler, lenin az önce verdiği “yer var” bilgisi geçerliliğini o iki satır arasında yitirmiş olur. len ve cap bu yüzden bir karar aracı değil, yalnızca bir gözlem aracıdır.

Bir Yer Açılınca Deneme Yeniden Geçer

Tamponun doluluğu sabit değildir — bir alıcı tampondan bir değer çektiğinde açılan yer, daha önce engellenen bir denemenin şimdi geçmesini sağlar. Bu da tek bir goroutine içinde, sırayla, hiçbir eşzamanlılık gerektirmeden gözlemlenebilir.

// alanAcilinca.go — tamponlu kanalda alici bir deger cekince kapanan yer tekrar aciliyor
package main

import "fmt"

func denenirMi(kanal chan int, v int) bool {
	select {
	case kanal <- v:
		return true
	default:
		return false
	}
}

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

	ucuncuDenemeDoluyken := denenirMi(kanal, 3)
	fmt.Println("tampon doluyken ucuncu deneme gecti mi:", ucuncuDenemeDoluyken)

	alinanDeger := <-kanal
	fmt.Println("alinan deger                          :", alinanDeger)

	ucuncuDenemeSonra := denenirMi(kanal, 3)
	fmt.Println("yer acilinca ayni deneme gecti mi      :", ucuncuDenemeSonra)
}
tampon doluyken ucuncu deneme gecti mi: false
alinan deger                          : 1
yer acilinca ayni deneme gecti mi      : true

GK11. — Kapasitesi 2 olan kanal iki değerle dolduruluyor; üçüncü deneme default’a düşüyor çünkü tampon dolu. <-kanal ile bir değer çekilince (1, çünkü kanal bir kuyruk gibi giriş sırasını koruyor — bu FIFO sıralaması tamponlu kanalın tanımının bir parçası, ayrı bir ölçüm değil) tamponda bir yer açılıyor. Aynı üçüncü deneme, kaynak kodu hiç değişmeden, şimdi true dönüyor. Kapasite sabit kalıyor; değişen şey kapasitenin doluluk oranı.

Buraya kadarki iki ölçüm select+default ile denedi; deneme başarısız olduğunda gönderim hiç yapılmıyor, program hemen bir sonraki satıra geçiyor. Ama tamponun dolmasının asıl sonucu, select kullanılmadan doğrudan yazılan bir gönderimin gerçekten beklemesidir — önceki derste tamponsuz kanalda görülen davranışın aynısı. Bunu tek bir goroutine içinde göstermek mümkün değil, çünkü bloke olan taraf kendi bloke olduğunu gözlemleyemez; ama bloke olmuş bir gönderimin, yer açılınca kesinlikle tamamlandığını ayrı bir sinyalle doğrulamak mümkün.

// blokluyor.go — kapasite dolunca blocking gonderim gercekten bekliyor, alici cekince tamamlaniyor
package main

import "fmt"

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

	ikinciTamamlandi := make(chan struct{})
	go func() {
		kanal <- 2
		close(ikinciTamamlandi)
	}()

	ilkAlinan := <-kanal
	<-ikinciTamamlandi
	ikinciAlinan := <-kanal
	fmt.Println("ilk alinan   :", ilkAlinan)
	fmt.Println("ikinci alinan:", ikinciAlinan)
}
ilk alinan   : 1
ikinci alinan: 2

GK12. — Kapasitesi 1 olan kanal tek değerle (1) dolduruluyor. Başlatılan goroutine’in kanal <- 2 satırı, tampon dolu olduğu için hemen tamamlanamaz ve orada bekler — ana goroutine bunu doğrudan gözlemlemiyor, çünkü gözlemlemeye çalışmak (örneğin select+default ile “hâlâ bloke mi” diye sormak) kendi başına bir yarış olurdu. Bunun yerine ana goroutine önce tampondaki tek değeri çekiyor (ilkAlinan), bu da ikinci goroutine’in bekleyen gönderimine yer açıyor; ardından <-ikinciTamamlandi ile o gönderimin kesinlikle tamamlandığını bekliyor. close(ikinciTamamlandi) çağrısı kanal <- 2 satırından sonra çalıştığı için, bu kapanmayı görmek, gönderimin de gerçekleştiğini garanti ediyor. Son alım (ikinciAlinan) bu yüzden her koşumda 2.

Sınırlayıcı Ölçüm: Tampon Eşzamanlamayı Kaldırmaz, Erteler

Önceki iki ölçüm tamponun ne zaman engellediğini gösterdi. Bu bölüm tamponun neyi söylemediğini ölçüyor: bir tamponlu kanala yapılan gönderim tamamlandığında, gönderen taraf o değerin gerçekten okunduğunu bilmiyor — yalnızca tamponda yer bulduğunu biliyor. Tamponsuz bir kanalda bu iki bilgi aynı satırda gelirdi, çünkü gönderim ancak alıcı hazır olduğunda tamamlanırdı. Tamponlu bir kanalda bu iki an ayrışıyor.

// ertelenir.go — tamponlu kanalda gonderim tamamlanir ama alicinin aldigini bildirmez
package main

import "fmt"

func main() {
	kanal := make(chan int, 1)
	onay := make(chan struct{})

	kanal <- 42
	fmt.Println("gonderim tamamlandi (tamponlu)")

	go func() {
		<-kanal
		close(onay)
	}()

	<-onay
	fmt.Println("alicinin aldigini simdi ogrendik (ayri sinyalle)")
}
gonderim tamamlandi (tamponlu)
alicinin aldigini simdi ogrendik (ayri sinyalle)

GK13.kanal <- 42 satırı, kapasite dolu olmadığı için hiç beklemeden tamamlanıyor ve hemen ardından “gönderim tamamlandı” satırı basılıyor — bu noktada henüz hiçbir alıcı goroutine bile başlamadı. Alıcının değeri gerçekten okuduğunu öğrenmenin tek yolu, ayrı bir onay kanalıdır; kod bunu kullanmasaydı, gönderen tarafın alımın ne zaman olduğunu (hatta olup olmadığını) bilmesinin hiçbir yolu kalmazdı. Bu, tamponlu bir kanalın gönderme tarafına verdiği garantiyle sınırını aynı anda gösteriyor: garanti yalnız “tampon bunu tuttu”, alım hakkında hiçbir söz içermiyor.

Bu fark, hangi kanalın seçileceği kararının bir mühendislik tercihi olduğunu gösteriyor. Gönderenin alımın gerçekleştiğini bilmesi gerekiyorsa — örneğin bir işin başladığını onaylamak için — tamponsuz bir kanal ya da ayrı bir onay sinyali gerekir. Gönderenin yalnızca değeri bırakıp devam etmesi yeterliyse, ve alıcının o anda hazır olması gerekmiyorsa, tampon göndereni bekletmeden ilerlemesini sağlar. İkisi de “eşzamanlama” yapıyor, ama farklı anlarda ve farklı bilgiyle.

Bu ayrım, “hazır olduğunu bilmek” ile “hazır olacağından emin olmak” arasındaki farkın bir örneği. onay kanalı olmasaydı, ertelenir.godaki gönderen taraf değeri tampona bıraktıktan sonra hiçbir şey öğrenmeden devam ederdi — ne alıcının çalıştığını, ne ne zaman çalışacağını, ne de hiç çalışıp çalışmayacağını. Tampon kapasitesi büyüdükçe bu belirsizlik ortadan kalkmıyor, yalnızca kaç gönderimin bu belirsizlikle bırakılabileceği büyüyor: kapasitesi 100 olan bir kanala art arda yüz gönderim yapmak, her biri ayrı ayrı “tamamlandı” der ama bunların kaçının gerçekten okunduğu konusunda tek bir bilgi vermez.

Tamponlu bir kanalın bu sessizliği hata değildir; kanalın sözleştiği şey zaten yalnızca “bu değeri sakla”, “alıcının onu aldığını bildir” değildir. İkinci bilgiyi isteyen kod, tıpkı bu ölçümde olduğu gibi, onu ayrıca kurmak zorundadır.

Bu dersin dört ölçümü tek bir çizgide birleşiyor. kapasite.go kapasitenin bir sınır olduğunu, o sınıra kadar hiç beklemeden geçilebildiğini gösterdi. lencap.go bu sınırın ve o anki doluluğun programın içinden okunabildiğini gösterdi. alanAcilinca.go ve blokluyor.go sınırın aşılmasının iki farklı görünümünü verdi: biri denemenin başarısız olup hemen devam ettiğini, öteki gerçek bir gönderimin yer açılana kadar gerçekten beklediğini. ertelenir.go ise bu dört ölçümün hiçbirinin, gönderimin alındığı anı söylemediğini gösterdi — kapasite, doluluk, engellenme, hepsi göndericinin kendi tarafında ölçülebilen şeyler; alım tarafının ne zaman gerçekleştiği bunların dışında kalıyor.

Özet

  • Kanalın kapasitesi, engellemenin başladığı noktayı belirler: ilk kapasite kadar gönderim hiç beklemeden geçer, sonrakiler alıcı gelene kadar bekler.
  • Kapasitesi sıfır olan kanal ayrı bir tip değildir; tamponsuz kanal, kapasitesi sıfır olan kanalın kendisidir.
  • Bir alıcı tampondan bir değer çektiğinde açılan yer, daha önce engellenmiş bir denemenin şimdi geçmesini sağlar.
  • Tamponlu bir kanala gönderim tamamlanmış olmak, alıcının o değeri okuduğu anlamına gelmez; bu bilgi yalnız ayrı bir sinyalle elde edilebilir.
  • Tamponsuz ve tamponlu kanal arasındaki seçim, gönderenin alımın gerçekleştiğini bilmesi gerekip gerekmediğine bağlı bir tasarım kararıdır.
  • len bir kanalda o an bekleyen değer sayısını, cap sabit kapasiteyi verir; ikisi birlikte kanalın anlık doluluğunu, ayrıca bir deneme yapmadan okumayı sağlar.

Sonraki Adım

Bu derste ölçülen iki kanal türü de yalnızca bir tek kanalı bekliyordu; farkları o kanalın ne zaman beklettiğindeydi. select+default bu derste yalnızca tek bir kanal üzerinde bir deneme aracı olarak kullanıldı — kapasitenin dolu olup olmadığını yoklamak için. Ama bir goroutine çoğu zaman birden çok kaynaktan aynı anda haber bekler: birkaç sonuç kanalından biri, bir iptal sinyali, bir zaman aşımı. Sıradaki ders selectin bu geniş biçimini ölçüyor — birden çok kanal aynı select içinde birlikte beklendiğinde hangi dal hazırsa o seçiliyor, ve hiçbiri hazır değilse ne oluyor.

İ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