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…