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
kapasitekadar 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.
lenbir kanalda o an bekleyen değer sayısını,capsabit 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.