Ders 02 / 14
Kanallar
Bir kanal aynı satırda iki iş birden yapar: bir değeri bir goroutine'den ötekine taşır ve iki goroutine'in aynı noktada buluşmasını zorlar. Tamponsuz bir kanala gönderim, karşı tarafta bir alıcı hazır olana kadar hiç tamamlanmaz; alıcı hiç yoksa deneme kalıcı olarak geçmez. Sınırlayıcı ölçüm: kanal bir kuyruk değildir, çünkü aktarılan değer kopyalanır ve gönderen tarafın gönderimden sonraki değişikliği alıcıya hiçbir zaman ulaşmaz.
İçindekiler
Önceki ders on iki goroutine’i dört durma biçimine ayırdı ve beş “beklediği geldi” satırının
ilkini tamponsuz bir kanaldan alımla kurdu: kanal <- 1 yazıldı, karşı taraftaki goroutine
bunu aldı ve durdu. O satırda aslında iki ayrı iş birden oldu — bir tam sayı bir goroutine’den
ötekine geçti, ve iki goroutine aynı anda aynı noktada buluştu. Bu ders bu ikisini ayırıyor:
kanal her iki işi de aynı satırda yapıyor, ama bu iki iş ayrı ayrı ölçülmeden “eşzamanlama”
sözcüğü ne söylüyor, “aktarım” sözcüğü ne söylüyor birbirine karışıyor.
Bir kanal, tipli bir değerin bir goroutine’den başka bir goroutine’e geçtiği tek yoldur;
chan int, chan string gibi yazılır ve <- işlecinin yönü gönderim ile alımı ayırır:
kanal <- deger bir gönderim, deger := <-kanal bir alımdır. Ama kanal yalnızca bir
taşıyıcı değildir. Aynı satırda ikinci bir iş daha yapar: göndereni ve alıcıyı
eşzamanlar. Bu ikinci iş, kanalı bir kuyruktan ya da bir posta kutusundan ayıran şeydir,
ve bu dersin ölçtüğü tam olarak budur.
Bu iki işin birlikte gelmesi Go’nun eşzamanlılığa yaklaşımının merkezinde duruyor: paylaşılan bir değişkene kilitle erişmek yerine, değeri doğrudan bir goroutine’den ötekine göndermek. Kilit ve paylaşılan durum bu kursun C payının konusu; bu ders yalnız iletişimin kendi düzeneğini — kanalın ne yaptığını ve ne yapmadığını — ölçüyor.
make(chan int) ile oluşturulan bir kanal başlangıçta boştur ve hiçbir goroutine’e
bağlı değildir; onu kullanan tarafların sayısı sıfır, bir ya da birçok olabilir. Bu ders
en basit durumu, tek gönderen ve tek alıcıyı, ölçüyor. Birden çok goroutine’in aynı kanalı
paylaştığı durumlar — birçok goroutine’in aynı sonuç kanalına yazması, ya da birçok
goroutine’in aynı iş kanalından okuması — B payının konusu; kanalın kendi kuralları burada
kurulduktan sonra oraya taşınıyor.
Alıcısız Gönderim Hiçbir Zaman Geçmez
Tamponsuz bir kanala (make(chan T), kapasitesiz) gönderim, karşı tarafta o değeri almaya
hazır bir goroutine olana kadar tamamlanmaz. Bunu süre geçirerek “muhtemelen kimse yok”
diye varsaymak yerine, select ile default dalını birlikte kullanıp denemek
mümkündür: gönderim o an geçebiliyorsa select o dalı seçer, geçemiyorsa hiç beklemeden
default’a düşer.
// bulusma.go — tamponsuz kanalin iki isi: deger aktarmak ve alicisiz gonderimi engellemek
package main
import "fmt"
func denenirMi(k chan string, v string) bool {
select {
case k <- v:
return true
default:
return false
}
}
func main() {
kanal := make(chan string)
fmt.Println("alici hic yokken deneme gonderimi gecti mi:", denenirMi(kanal, "ilk deneme"))
teslimAlindi := make(chan string)
go func() {
deger := <-kanal
teslimAlindi <- "alici aldi: " + deger
}()
kanal <- "merhaba"
fmt.Println("gonderim tamamlandi, alici degeri teslim aldi")
fmt.Println(<-teslimAlindi)
}
alici hic yokken deneme gonderimi gecti mi: false gonderim tamamlandi, alici degeri teslim aldi alici aldi: merhaba
GK4. — İlk satırda kanal üzerinde henüz hiçbir goroutine beklemiyor; denenirMi bu
durumda her koşumda false döner, çünkü ortada bekleyen kimse yoktur — bu bir zamanlama
sorunu değil, tamponsuz bir kanalın tanımının doğrudan sonucudur. İkinci bölümde goroutine
başlatılıyor ve kanal <- "merhaba" bu kez select içinde değil, doğrudan yazılıyor: bu
satır alıcı gelene kadar bloklanır ve alıcı geldiğinde tamamlanır. Aradaki fark önemli:
select+default bir anlık durumu yokluyor, düz kanal <- değer ise gerektiği kadar
bekleyip kesinlikle tamamlanana kadar durur.
Programın geri kalanının tek bir goroutine’de (ana goroutine’de) çalıştığına dikkat
edilmeli: alıcı goroutine ekrana hiçbir şey yazmıyor, yalnız aldığı değeri teslimAlindi
kanalına geri gönderiyor. Bütün fmt.Println çağrıları ana goroutine’dedir ve aralarında
kanal üzerinden zorlanmış bir sıra vardır — bu yüzden çıktı her koşumda birebir aynıdır.
Bu ölçüm biçimi bu dersin ve kursun geri kalanının kuralıdır: bir goroutine kendi başına ekrana yazmaz, yalnız bir kanala yazar; ekrana yazma işi hep ana goroutine’de, kanaldan okunan değer üzerinden yapılır. İki goroutine aynı anda ekrana yazsaydı, satırların hangi sırayla çıkacağı yine goroutine’lerin kendisi kadar belirsiz olurdu — kanal burada yalnız değer taşımak için değil, aynı zamanda çıktının sırasını tek bir goroutine’e toplamak için de kullanılıyor.
Aktarılan Değer Kopyalanır
Kanal bir değer taşırken o değerin kopyasını taşır, referansını değil. Bu, M09/K01 Go Temelleri kursunda dizi ve dilim atamalarında görülen kopya/paylaşım ayrımının kanallardaki karşılığıdır: bir kanala bir yapı değeri gönderildiğinde, göndericinin o değişkeni sonradan değiştirmesi alıcının elindeki kopyayı etkilemez.
// kopyalanir.go — kanaldan aktarilan deger kopyalanir, gonderenin sonraki degisikligi aliciya ulasmaz
package main
import "fmt"
type kayit struct {
Ad string
Deger int
}
func main() {
kanal := make(chan kayit)
sonuc := make(chan kayit)
go func() {
alinan := <-kanal
sonuc <- alinan
}()
k := kayit{Ad: "sayac", Deger: 1}
kanal <- k
k.Deger = 999
fmt.Println("gonderenin kendi degeri simdi:", k)
fmt.Println("alicinin teslim aldigi deger :", <-sonuc)
}
gonderenin kendi degeri simdi: {sayac 999}
alicinin teslim aldigi deger : {sayac 1}
GK5. — kanal <- k satırı tamamlandığında k‘nin o andaki değeri kopyalanıp
alıcı goroutine’e geçti. Hemen ardından k.Deger = 999 yazıldığında değişen şey yalnızca
gönderenin kendi yerel değişkeni; alıcının aldığı kopya bundan habersizdir ve Deger
alanı 1 olarak kalır. Bu davranış kayitin bir işaretçi değil bir değer tipi olmasından
geliyor — kanaldan işaretçi geçirilseydi (chan *kayit), kopyalanan şey işaretçinin
kendisi olur, gösterdiği veri değil; o durumda gönderenin değişikliği alıcıdan da görünür
olurdu. Bu ders yalnız değer aktarımını ölçüyor; işaretçilerin kopyalanmasıyla neyin
paylaşılıp neyin paylaşılmadığı Diziler ve Dilimler dersinde ayrı bir eksendi ve burada
tekrarlanmıyor.
Bu kopyalama kuralı hiçbir istisna taşımaz: kanalın kendi tipi ne olursa olsun — tam sayı,
dizgi, yapı, hatta bir başka kanal — gönderilen şey her zaman o değerin o andaki bit
kalıbının bir kopyasıdır. Kanal, gönderilen değere hiçbir zaman kendi tarafından bir
işaretçi eklemez; paylaşım isteniyorsa, tıpkı kayitin yerine *kayit göndermekte olduğu
gibi, bu paylaşımı isteyen tarafın kendi tipini seçmesi gerekir.
Kopya Yüzeyseldir: İçindeki Dilim Paylaşılır
Kopyalanan şey her zaman yalnızca yapının kendisidir, yapının içindeki her alan değil. Bir yapı bir dilim alanı taşıyorsa, kanal o yapıyı kopyalarken dilimin başlığını kopyalar — uzunluk, kapasite ve alt diziye işaretçi — ama alt dizinin kendisini kopyalamaz. Bu, M09/K01 Go Temelleri kursunda diziler ve dilimlerde görülen yüzeysel kopya davranışının kanal üzerinden aktarımdaki karşılığı.
// sigkopya.go — kanaldan giden yapi kopyalanir ama icindeki dilim ayni alt diziyi paylasir
package main
import "fmt"
type sepet struct {
Etiket string
Ogeler []int
}
func main() {
kanal := make(chan sepet)
sonuc := make(chan sepet)
go func() {
alinan := <-kanal
alinan.Ogeler[0] = 999
sonuc <- alinan
}()
s := sepet{Etiket: "kaynak", Ogeler: []int{1, 2, 3}}
kanal <- s
alinanKopya := <-sonuc
fmt.Println("alicinin kopyasi :", alinanKopya)
fmt.Println("gonderenin kendi degiskeni:", s)
}
alicinin kopyasi : {kaynak [999 2 3]}
gonderenin kendi degiskeni: {kaynak [999 2 3]}
GK6. — alinan kanaldan gelen sepet değerinin kendi kopyasıdır ve Etiket alanı
gönderenden bağımsızdır — biri değiştirilse öteki etkilenmez, tıpkı bir önceki ölçümdeki
kayit.Deger gibi. Ama Ogeler alanı bir dilim olduğu için kopyalanan şey yalnızca üç
sözcüklük dilim başlığıdır; başlığın işaret ettiği alt dizi göndericiyle aynıdır.
alinan.Ogeler[0] = 999 yazıldığında değişen bellek konumu, iki değişkenin de paylaştığı
o tek alt dizi konumudur — bu yüzden s.Ogeler[0] da 999 olarak görünüyor, oysa s
değişkeninin kendisine hiçbir goroutine doğrudan dokunmadı. Bu satırın ana goroutine’de
güvenle okunabilmesinin sebebi, <-sonuc alımının kanal işleminin kendi eşzamanlama
sözü gereği alt dizideki yazmayı da beraberinde görünür kılması; kanal yalnız sonuc
değerini değil, ona kadar olan bütün belleği de taşıyor.
Bu, “kanaldan aktarılan değer kopyalanır” cümlesinin sınırını gösteriyor: kopyalama
yapının kendisiyle biter, yapının içindeki dilim, sözlük ya da işaretçi alanlarının
gösterdiği veriyle sürmez. Bir yapıyı kanaldan bağımsız bir kopya olarak göndermek
isteyen kod, dilim alanını da ayrıca copy ile kopyalamak zorundadır — kanal bunu
kendiliğinden yapmaz.
Kanalın Yönü Tipin Bir Parçası Olabilir
Bir kanal değişkeni her iki yönde de kullanılabilir, ama bir fonksiyonun imzasında kanalın
yönü kısıtlanabilir: chan<- int yalnızca gönderime izin veren bir kanal tipidir, <-chan int yalnızca alıma izin veren. Bu kısıtlama tipin kendisinin bir parçasıdır — aynı
make(chan int) ile oluşturulan çift yönlü bir kanal, yönlü bir parametreye geçirildiğinde
o fonksiyon içinde yalnız izin verilen yönde kullanılabilir.
// yonlu.go — kanal yonu tipin parcasi olabilir: sadece gonderen, sadece alan
package main
import "fmt"
func uret(hedef chan<- int, adet int) {
for i := 1; i <= adet; i++ {
hedef <- i
}
close(hedef)
}
func topla(kaynak <-chan int) int {
toplam := 0
for v := range kaynak {
toplam += v
}
return toplam
}
func main() {
kanal := make(chan int)
go uret(kanal, 5)
fmt.Println("uretilen bes degerin toplami:", topla(kanal))
}
uretilen bes degerin toplami: 15
GK7. — uret fonksiyonuna geçirilen kanal, çift yönlü bir değişken olmasına rağmen
uretin içinde yalnızca hedef chan<- int olarak görünüyor; uretin gövdesinde <-hedef
yazmaya çalışmak derlenmez. Aynı şekilde toplanın içinde kaynak <- 1 gibi bir gönderim
denemesi de derlenmez. Bu, kanalın hangi tarafının hangi işi yapacağını imzada okunur kılan
bir yazım kararıdır: uretin imzasına bakan biri, onun kanala yalnız yazacağını, hiç
okumayacağını kaynağı okumadan bilir. uretin kendisi hedefi kapatıyor da — kapatma
sorumluluğunun kimde olduğu bu dersin değil, konu kapanış dersinin ölçtüğü şey.
Yön kısıtlaması çalışma zamanında hiçbir şey değiştirmiyor; chan<- int ile <-chan int
altta yatan aynı kanal değerini paylaşıyor, farklı bir kanal türü değiller. Kazanılan şey
yalnızca derleme zamanında okunabilirlik: bir fonksiyonun imzası, o fonksiyonun kanalla
ne yapacağını taahhüt ediyor, ve bu taahhüdü bozan bir çağrı derlenmiyor. main içindeki
kanal değişkeni hem urete hem toplaya aynı çift yönlü hâliyle geçiyor; kısıtlama
yalnız o fonksiyonların kendi içinde geçerli oluyor.
İki Ölçüm Bir Arada: Taşımak ve Buluşturmak
GK8. — Bu iki ölçüm bir arada okunduğunda kanalın gerçek sözü ortaya çıkıyor: bir kanal işlemi hem bir değerin o anki hâlini dondurup taşır, hem de göndereni ve alıcıyı aynı noktada buluşturur. Kanalı bir kuyruğa benzetmek ilk bakışta kullanışlı bir sezgi verir — değerler bir uçtan girer, öteki uçtan çıkar — ama bir kuyruk gönderenle alıcıyı hiç buluşturmaz, yalnızca bir arabellek üzerinden ayırır. Tamponsuz bir kanalda böyle bir arabellek yoktur; buluşma tamponun yerini alır. Sıradaki iki ders bu buluşmanın bir tampon eklendiğinde nasıl ertelendiğini ölçüyor.
Bu iki iş her zaman aynı ağırlıkta gelmiyor. Bazı kullanımlarda önemli olan yalnız değerin
kendisi — bir hesabın sonucu, bir kaydın içeriği. Bazı kullanımlarda ise değerin ne
olduğu hiç önemli değildir, önemli olan yalnızca bir şeyin olduğu bilgisi; bu durumda
chan struct{} gibi hiçbir veri taşımayan bir tip tercih edilir ve kanal yalnız
eşzamanlama işini görür. Bu dersin bulusma.go örneğindeki teslimAlindi kanalı ilk
türden, ölçümün geri kalanında birkaç kez göreceğimiz kapatma sinyalleri ikinci türdendir.
Kanalın bir de yazılmayan durumu var: bir kanal değişkeninin sıfır değeri nildir ve
nil bir kanal üzerindeki gönderim ile alım hiçbir zaman tamamlanmaz, sonsuza kadar bloke
kalır. Bu davranış tanımlıdır ve M09/K01 Go Temelleri kursunda sıfır değerler bağlamında
bir cümleyle geçti; bu kurs onu tekrar kurmuyor. Ama bir nil kanalın “sonsuza kadar
hazır olmama” özelliği, select içinde kullanışlı bir sonuç doğuruyor: nil bir kanalı
içeren bir dal, o kanal başka bir değerle değiştirilmeden asla seçilmez — bu yüzden bir
dalı geçici olarak “kapatmak” için o dalın kanalını nile çevirmek yeterlidir.
// nilkanal.go — nil kanal uzerindeki her islem sonsuza kadar bloke kalir, select icinde hep pas gecilir
package main
import "fmt"
func main() {
var nilKanal chan int
hazir := make(chan int, 1)
hazir <- 7
select {
case v := <-nilKanal:
fmt.Println("nil kanaldan alindi:", v)
case v := <-hazir:
fmt.Println("hazir kanaldan alindi:", v)
default:
fmt.Println("hicbiri hazir degil")
}
}
hazir kanaldan alindi: 7
nilKanal hiçbir zaman make ile ilklendirilmedi, bu yüzden sıfır değeri olan nilde
kaldı. select üç dalı da yokladı: nilKanal dalı hiçbir zaman hazır olamayacağı için
elenir, hazir dalı doludur ve seçilir. nilKanal üzerinde doğrudan <-nilKanal yazılıp
select dışında çağrılsaydı program sonsuza kadar orada kalırdı; bu ders o durumu hiç
koşturmuyor, çünkü sonucu (sonsuz bloke) gözlemlenebilir bir çıktı üretmez. select
deyiminin kendisi bir sonraki derste ayrıntılı ölçülüyor; burada önemli olan, nil bir
kanalın bir dalı etkisiz kılmanın bir yolu olarak kullanılabildiği.
Özet
- Bir kanal aynı satırda iki iş yapar: bir değer taşır ve göndereni ile alıcıyı eşzamanlar; ikisi birlikte gelir.
- Tamponsuz bir kanalda alıcı yokken gönderim denemesi (
select+default) hiçbir zaman geçmez; bu bir zamanlama tesadüfü değil, tanımın kendisidir. - Doğrudan yazılan bir gönderim (
kanal <- değer) alıcı gelene kadar bloklanır ve alıcı geldiğinde kesin olarak tamamlanır. - Kanaldan aktarılan bir değer kopyalanır; gönderenin gönderimden sonraki değişikliği alıcıya hiçbir zaman ulaşmaz.
nilbir kanal üzerindeki her işlem sonsuza kadar bloke kalır; bu davranış M09/K01’de kurulmuş bir tanımlılığın kanallardaki karşılığıdır.
Sonraki Adım
Bu derste ölçülen tamponsuz kanal, göndereni alıcı hazır olana kadar bekletiyordu — buluşma her seferinde tam o anda kuruluyordu. Bir kanala kapasite eklendiğinde bu zorunluluk ortadan kalkmıyor, yalnız erteleniyor. Sıradaki ders tamponun engelleme davranışını nerede değiştirip nerede değiştirmediğini ölçüyor: kapasite dolana kadar gönderim hiç beklemeden geçiyor, dolduğunda yine bekliyor.
Sorulacak soru şu: buluşma zorunluluğu tamamen kalkıyor mu, yoksa yalnızca ne zaman gerçekleşeceği mi değişiyor? Bu sorunun yanıtı, tamponlu bir kanala gönderimin hemen dönmesiyle, o değerin hâlâ okunmayı beklediği gerçeğinin birbirinden ayrılmasında yatıyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.