İçeriğe geç
academia.sh

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.
  • nil bir 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat