İçeriğe geç

Channels

Channels (kanallar), Go’da goroutine’ler arasında veri alışverişini sağlamak için kullanılan yapılar olarak tanımlanabilir. Kanallar sayesinde, bir goroutine başka bir goroutine’e veri gönderebilir veya ondan veri alabilir. Kanallar, genellikle eşzamanlı işlemler arasında veri paylaşımı için kullanılır ve Go’nun eşzamanlı programlama modelinin temel yapı taşlarından biridir.

Özet olarak kanallar boru olarak düşünülebilir. Bir taraftan veri gönderilirken, diğer taraftan veri alınabilir.

Channels

Kanalların Kullanımı

Kanallar make fonksiyonu ile oluşturulur.

ch := make(chan int)

Yukarıdaki örnekte içerisinden int tipinde veri aktarılabilen ch isminde bir kanal oluşturduk.

Unbuffered Channels (Arabelleksiz Kanallar)

Önceki örneğimizde tanımladığımız gibi arabelleksiz kanallar make fonksiyonu ile tanımlanırken sadece veri aktaracağı tip belirtilir.

1
package main
2
3
import (
4
"fmt"
5
"time"
6
)
7
8
func main() {
9
ch := make(chan string)
10
11
go func() {
12
time.Sleep(1 * time.Second)
13
ch <- "hello" // 16. satırdan teslim alınana kadar bekleyecek
14
}()
15
16
a := <-ch // 13. satırdan gönderilene kadar bekleyecek
17
18
fmt.Println(a) // hello
19
}

Burada kanal kullanımı için bir örnek görmekteyiz. Kanallara veri gönderilirken veya okunurken <- operatörü kullanılır. Ok operatörü kanalı gösteriyorsa (ch <- "hello") kanala değer gönderilir. Eğer ok kanalın dışına doğru ise (a := <-ch) kanaldan veri okunur.

ch <- "selam"

Yukarıdaki örnekte ch adlı kanala string tipinde değer gönderdik. Arabelleksiz (unbuffered) kanallarda veri gönderiminin tamamlanması için gönderilen verinin kanalın diğer ucundan teslim alınması gerekir. Aksi taktirde içerisinde çalışılan goroutine gönderme işlemi tarafından bloklanır.

a := <-ch

Örneğimizde ch adlı kanaldan gelen veriyi a isminde değişkene atadık. Kanala gönderilen değer string tipinde olduğu için a değişkeninin tipi de string kabul edilir. Programımız ch kanalından değer gelene kadar bekleyecektir.

Kanallardan okunan değerleri bir değişkene atamadan, sadece veri gelmesini beklemek istiyorsak aşağıdaki gibi kullanabiliriz.

<-ch

Buffered Channels (Arabellekli Kanallar)

Arabellekli kanallar, aynı esnada belirlenen sayıda veri aktarabilen kanallardır. Tanımlanırken boyut belirtilir.

1
package main
2
3
import (
4
"fmt"
5
"time"
6
)
7
8
func main() {
9
ch := make(chan string, 4)
10
11
go func() {
12
ch <- "msg1"
13
ch <- "msg2"
14
ch <- "msg3"
15
ch <- "msg4"
16
fmt.Println("all msg sent")
17
}()
18
19
time.Sleep(2 * time.Second)
20
a := <-ch
21
b := <-ch
22
c := <-ch
23
d := <-ch
24
25
fmt.Println(a, b, c, d) // msg1 msg2 msg3 msg4
26
}

Yukarıdaki örnekte aynı anda 4 adet string tipinde veri taşıyabilen bir kanal tanımlanmıştır. 12-15. satırlar arasında kanala değerler gönderilir. 5. kez değer gönderilmek istendiğinde programın bloklanmaması için kanaldan 1 değerin okunması gerekir. 20-23. satırlar arasında ise kanaldan değerler okunur. Dolayısıyla kanala yeni değerler gönderilmesi için yer açılmış olur.

Canlı Demo

Kanal

Kanalların Kapatılması

Kanallar kullanılmadığında kapatılması gerekir. Bir kanalı kapatmak için close fonksiyonu kullanılır.

ch := make(string)
.
.
.
close(ch)

veya içerisinde çalışılan fonksiyon tamamlandığında kapatılması için defer ile kullanabiliriz.

ch := make(string)
defer close(ch)

Kanalın kapatılmasının hali hazırda dinleme yapan yerler için etkilerine bakalım.

1
ch := make(chan string, 3)
2
3
go func() {
4
time.Sleep(2 * time.Second)
5
ch <- "msg1"
6
close(ch)
7
}()
8
9
a := <-ch // "msg1" gelecek
10
b := <-ch // "" (boş string) gelecek
11
12
fmt.Println(a, b) // "msg1" ""

Yukarıdaki örneğe göre 5. satırda yazılan değer 9. satırda okunuyor. Devamında 6. satırda kanal kapatılıyor, bu yüzden 10. satırda okuma işlemi kanal kapatıldığından pas geçiliyor ve kanal string veri taşıdığından string’in varsayılan değeri olan "" (boş string) b değişkenine atanıyor.

Okuyucu tarafından boş string alındığında (örneğe göre 10. satır) boş string’in gelmesinin sebebi kanalın kapanması mı yoksa kanala gerçekten de boş string mi yollanmış bunu anlamak için comma ok kullanabiliriz.

1
ch := make(chan string, 3)
2
3
go func() {
4
time.Sleep(2 * time.Second)
5
ch <- ""
6
close(ch)
7
}()
8
9
a, ok1 := <-ch // "", true
10
b, ok2 := <-ch // "", false
11
12
fmt.Println(a, ok1, b, ok2) // "" true "" false

5. satırda kanala bu sefer boş string değerini yolladık. 9. satırda kanaldan değer okunduğunda kanal açık olduğu için ok1 değeri true geldi. 10. satırda ise kanal kapalı olduğu için ok2’nin değeri false geldi. comma ok kullanarak kanaldan gelen varsayılan değerin kanal kapandığı için mi, yoksa boş string yollandığı için mi geldiğini anlayabildik.

Range ile Kanaldan Değer Okunması

range kullanılarak doğal olarak kanallar kapatılana kadar değer okuma (dinleme) işlemi yapılabilir.

main.go
1
package main
2
3
import (
4
"fmt"
5
)
6
7
func main() {
8
ch := make(chan string)
9
10
go func() {
11
ch <- "msg1"
12
ch <- "msg2"
13
ch <- "msg3"
14
close(ch)
15
}()
16
17
for v := range ch {
18
fmt.Println(v)
19
}
20
21
fmt.Println("all msg received")
22
}

ch isimli kanalımızı range ile kullanarak kanala yazılan (gönderilen) değerleri okuyabiliriz. 11, 12 ve 13. satırlarda değerlerimizi gönderdik. Devamında kanalı close kullanarak kapattık. Böylelikle kanalı dinleyen yerler artık kanala bir değer gönderilmeyeceğini bilip dinlemeyi pas geçecektir.

  1. satırda ise range ile beraber ch kanalına gönderilen değerleri okuduk. Kanal kapatılana kadar okuma işlemine devam edildi.

Çıktımız aşağıdaki gibi olacaktır.

Çıktımız
msg1
msg2
msg3
all msg received

Kanala Yazma veya Okuma İşlemlerinde Deadlock

Deadlock, iki veya daha fazla goroutine’nin birbirlerini sonsuza dek beklemesinden meydana gelen bir durumdur. Deadlock go runtime’ı tarafından tespit edilebilir. Go runtime deadlock’ı tespit ettiğinde fatal error vererek programdan çıkış yapar.

Yukarıdaki örneği bir de deadlock hatası verecek şekilde yeniden kurgulayalım.

1
package main
2
3
import (
4
"fmt"
5
)
6
7
func main() {
8
ch := make(chan string)
9
10
go func() {
11
ch <- "msg1"
12
ch <- "msg2"
13
ch <- "msg3"
14
// close(ch)
15
}()
16
17
for v := range ch {
18
fmt.Println(v)
19
}
20
21
fmt.Println("all msg received")
22
}

Yukarıdaki örnekte bu sefer 14. satırda kanalı kapatmıyoruz. Bu yüzden kanalı dinleyen yerler sonsuza kadar dinlemede takılı kaldığı için go runtime deadlock tespit edip fatal error verecektir.

Çıktımızı görelim.

msg1
msg2
msg3
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/path/to/example/project/main.go:17 +0xc4
exit status 2

Çıktıya göre main.go:17 satırında deadlock gerçekleştiği belirtiliyor.

Bu sefer yazma esnasında deadlock gerçekleşen bir örnek görelim.

main.go
1
package main
2
3
import (
4
"fmt"
5
)
6
7
func main() {
8
ch := make(chan string)
9
10
go func() {
11
for v := range ch {
12
fmt.Println(v)
13
if v == "msg2" {
14
break
15
}
16
}
17
}()
18
19
ch <- "msg1"
20
ch <- "msg2"
21
ch <- "msg3"
22
}

Örneğimizde yazma ve okuma yerlerini değiştirdik. döngü içerisinde kanalı dinliyorken, eğer kanaldan msg2 değeri gelirse döngüyü sonlandırması için bir ekleme yaptık. Bu sayede msg3 değeri gelmeden önce kanalı dinlemeyi kestiğimiz için 21. satırda yeni değer yollanırken kanalı dinleyen bir yer bulunmayacak ve program bloklanacak.

Go runtime’ı deadlock’u tespit edip falat error verecek.

msg1
msg2
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/path/to/example/project/main.go:21 +0x9c
exit status 2

Çıktımızdaki hatada deadlock’un 21. satırda gerçekleştiğini belirtiyor. Arabelleksiz kanallarda programın bloklanmaması için kanala yazma işlemi yapılırken karşıdan okunması gerekir. Kanaldan okuma yapılana kadar yazma işlemi beklenir. Örneğimizde okuma yapacak kısım bulunmadığı için deadlock gerçekleşti.

Kanallarda Okuma veya Yazma Kısıtlamaları

Kanallar kullanılması için bir yere gönderilirken gönderildiği yer için okuma veya yazma kısıtlaması içerebilirler. Kısıtlama olmadan göndermek için,

func doSomething(ch chan string) {
// ch <- "msg" // yazma
// veya
// a := <- ch // okuma
}

Sadece-Okuma Modu

Sadece-Yazma Modu

Okuma veya yazma kısıtlamaları fonksiyon return’lerinde de kullanılabilir.

func doSomething() chan string // Kısıtlama olmadan
// veya
func doSomething() <-chan string // Sadece-okuma modu
// veya
func doSomething() chan<- string // Sadece-yazma modu

Örneklerde belirtilen okuma veya yazma sınırlamasının faydalarına değinelim.

  • Kanalın sadece belirli bir amaç için kullanıldığını göstermek, kodu daha okunur ve anlaşılır hale getirir. Örneğin bir fonksiyonda parametrelerde veya return’de kanalın kısıtlama ile belirtilmesi niyeti daha açık hale getirir.

  • Okuma veya Yazmayı sınırlandırmak paralel programlamayı daha güvenilir bir hale getirebilir. Örneğin fonksiyon return’ünde dinleme amacı ile kullanıcak bir kanalı, sadece-okuma modu ile return edersek, bu kanala fonksiyon dışarısından yazılmasına engel olacaktır. Bu da kanala yetkisiz müdahaleler yapılmasını engeller.