Channels

ud-214

Go tarafından concurrency problemlerin çözümü için önerilen yöntem channels tır. Bunları FIFO gibi hayal edebiliriz. Önce aşağıdaki koda bir bakalım.

package main

import "fmt"

func main() {
	c := make(chan int)

	c <- 42

	fmt.Println(<-c)
}

Öncelikle söyleyeyim ki yukarıdaki kod çalışmamaktadır ama buna sonra geleceğiz. İlk olarak c := make(chan int) ile int türden veri tutup, taşıyabilen c isminde bir channel oluşturduk. <- bir unary operatördür.[1] Burada c <- 42 dediğimiz zaman kanala 42 değerini koymuş oluyoruz. fmt.Println(<-c) ile channeldan okuma yapıyoruz. Normalde bunu yazdırdığımız zaman 42değerini görmeyi bekleriz ama kodu çalıştırınca aşağıdaki hatayı alıyoruz.

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/tmp/sandbox1871472892/prog.go:8 +0x36

Peki yukarıdaki kod neden bu hatayı verdi?

Biz make(chan int) ile bir kanal yarattığımız zaman adedta kapasitesi 0 olan bir FIFO yaratmış olduk. Bu mesela bir bayrak yarışına döndü. Yani bayrağı veren kişi ile alan kişinin aynı noktada buluşması lazım. Gönderen satır 42 yi kanala koymak istiyor ama o sırada kanalı dinleyen kimse yok, buffer da 0 kapasiteli, bu durumda dinleyen biri gelene kadar main()i çalıştıran goroutine bloke oluyor. Onu blokeden çözecek şey bir sonraki satırda <-c ile kanaldan yapılması fakat o satıra hiçbir zaman giremediği için bir deadlock oluşuyor. Güzel olan go runtime’ın bunu algılaması. C gibi dillerde ekrana boş boş bakarak bekleyebilirdik, varsayılan algılama mekanizması her dilde olmuyor.

Bknz: rendezvous

Kanallar bloklar!

Channel Buffer Capacity

make ile kanal oluşturken ikinci argüman olarak channel buffer capacity ya da channel capacity verebiliriz.

make(T, n)       channel           buffered channel of type T, buffer size n

Eğer yukarıdaki kodda c := make(chan int, 1) dersek en azından 1 adet int tutabilen bir channel yaratmış olduk. Bizim durumda en az 1 yeterli ama daha fazla da verebilirdik. Bu durumda kodumuz çalışacak ve 42 yazısını ekranda göreceğiz.

Ama eğer kapasitesi 1 eleman bir kanal oluşturup bu sefer iki adet int sokmaya çalışırsak, aşağıdaki kod gibi, yine deadlock oluşacaktır.

package main

import "fmt"

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

	c <- 42
	c <- 43 //Deadlock

	fmt.Println(<-c)
}

Alltaki kodda ise problem yoktur çünkü doldurup boşaltıyoruz.

package main

import "fmt"

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

	c <- 42
	fmt.Println(<-c)

	c <- 43
	fmt.Println(<-c)
}

Goroutine

Şimdi kanal kapasitemizi tekrar 0 yaparak alternatif bir çözüme bakalım.

package main

import "fmt"

func main() {
	c := make(chan int)

	go func() {
		c <- 42
	}()

	fmt.Println(<-c)
}

Yukarıdaki kod çalışmaktadır. Neden? Çünkü bu sefer kanala veri koyma işlemini ayrı bir goroutine’e verdik. Akışta şöyle olmasını bekleriz. Yarattığımız goroutine kanala veri koyma noktasına gelince eğer <-c ile okuma satırı main()i çalıştıran goroutine’de gelmediyse bloke olacaktır çünkü alabilecek kimse yok. Ama artık main() çalışmaya devam edebilmektedir çünkü ikisi farklı goroutine. Kanaldan okuma yapılacak an ile yazılacak an concurrent ilerlediği için bayrak yarışında alan ile veren elin buluşması gibi iki goroutine aynı noktada olabilecek ve veri transferi gerçekleşecektir.

Bu durum tek çekirdekli durumlarda da geçerlidir. Yani goroutine’ler paralel olmak zorunda değildir, concurrent olması yeterlidir. Go runtime bunun çaresine bakmaktadır.

Mesela aşağıdaki kodda önceki notlarda yaptığımız gibi runtime.GOMAXPROCS(1) ile paralel çalışmayı kapatalım. Concurrent tasarımımızdan dolayı tek çekirdek çalışsak bile yine programımız çalışmaktadır.

package main

import (
	"fmt"
	"runtime"
)

func main() {
	runtime.GOMAXPROCS(1)
	c := make(chan int)

	go func() {
		c <- 42
	}()

	fmt.Println(<-c)
}

Burada aynı anda tek bir goroutine çalışabilir. Muhtemelen olan, yarattığımız goroutine’nin hemen çalışmamasıdır çünkü main zaten çalışmaya devam etmektedir. main(), c den bir şey okumaya çalışınca c de bir şey olmadığı için bloke olur ve muhtemelen yarattığımız goroutine o noktada çalışır. Günün sonunda bu detaylar bizi çok da ilgilendirmez tasarımımızı doğru yaptığımız sürece. Çünkü go runtime interleaved bir çalışma da yaptırtabilir, garanti edilen şey bu kodun çalışacağıdır.


runtime.GOMAXPROCS(1) olduğu yani paralel çalışmama durumunu biraz daha ele alalım. Burada biliyoruz ki ya kanala gönderme işlemi ya da alma işlemi aynı anda olmayacak, illa ki biri erken olacak ve ilk olan bloke olacak. İşte bu durumda bloke goroutine aslında “akıllıca” bloke ediliyor. Çizelgeliyici bloke ederken aslında o goroutine’nin bir nevi neden bloke olduğunu da tutuyor. Örneğin gönderen bloke olduysa c kanalının alıcısı olmadığı için bloke edildi ya da alan bloke oluyorsa c kanalında veri olmadığı için bloke oldu gibi ek veriler de saklanıyor. Diyelim ki alan taraf önce bloke oldu ve gönderen koda geldiği zaman c yi dinleyen biri olduğu için artık gönderene bloke etmiyor ve gönderen ile alanı bayrak yarışı gibi buluşturuyor. Hatta optimizasyon kapsamında gönderen goroutine bu veriyi doğrudan alan gorutine’nin stack alanına yazabiliyor, yani önce kanala sonra tekrar alıcıya kopyalama yapılmayabiliyor. Konuştuğmuz gibi, buralar ilk aşamada takılmamız gereken noktalar değil.

O yüzden kanallar sadece bir posta kutusu değil aynı zamanda birer buluşma noktasıdır.

Bunların akra planda nasıl yapıldığı artık programlama dili tasarımı gibi konulara girse de hchan ya da sudog gibi veri yapıları incelenebilir.


Genelde önerilen best practice olarak unbuffered channel yani buffer boyutu 0 olan kanalların kullanılmasıymış. Buffered’a ihtiyacın oluyorsa genelde kurduğun yapıda bir şeyler yanlış olabilir diyorlarmış, onları yalancısıyım…