Go'da eşzamanlılık: goroutine ve channel pratiği
Go'nun eşzamanlılık modelini — goroutine ve channel'ı — uygulamalı kavramak. PHP'nin süreç modelinden gelince ne değişiyor?
Go öğrenmeye başladığımdan beri beklediğim konu buydu: concurrency. PHP’de paralel iş yürütmek için ya birden fazla process açarsınız ya da bir kuyruk sistemi kullanırsınız. Go’nun bu problemi dil düzeyinde nasıl ele aldığını görmek istiyordum.
Birkaç haftadır goroutine ve channel ile uğraşıyorum. Teorik anlayışla pratik kullanım arasındaki mesafeyi kapatmak zaman alıyor; ama zihin modelim oturmaya başladı.
Goroutine nedir?
Goroutine, Go’nun hafif eşzamanlı yürütme birimidir. Bir işletim sistemi thread’i değil — Go runtime’ı binlerce goroutine’i az sayıda thread üzerinde yönetiyor. Başlatmak için go anahtar kelimesi yeterli:
package main
import (
"fmt"
"time"
)
func selamlama(isim string) {
fmt.Printf("Merhaba, %s!\n", isim)
}
func main() {
go selamlama("Ali")
go selamlama("Ayşe")
go selamlama("Mehmet")
// Ana goroutine bitmeden diğerleri çalışsın:
time.Sleep(100 * time.Millisecond)
}
Bu örnek çalışıyor ama time.Sleep ile beklemek kötü bir pratik. Goroutine’lerin bitmesini sync.WaitGroup ile beklemek daha doğru:
package main
import (
"fmt"
"sync"
)
func isGor(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("İş %d tamamlandı\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go isGor(i, &wg)
}
wg.Wait()
fmt.Println("Tüm işler bitti.")
}
defer wg.Done() goroutine bitince WaitGroup sayacını düşürüyor. wg.Wait() ise sayaç sıfıra inene kadar bloklıyor.
Goroutine sızıntısı — sık yapılan bir hata
PHP’den gelince goroutine’lerin “otomatik kapandığını” sanmak kolay. Öyle değil. Bir goroutine hiç bitmeyecek bir kanaldan okuma veya yazma bekliyorsa sonsuza kadar çalışmaya devam eder — siz farkında olmasanız bile. Buna goroutine leak deniyor.
// Tehlikeli: goroutine asla bitmez
go func() {
result := <-ch // ch hiç kapatılmazsa burada takılır
fmt.Println(result)
}()
Bunun önüne geçmenin pratik yolu context.Context kullanmak veya kanalı doğru kapatmak. İlk birkaç haftada bu tuzağa birkaç kez düştüm; goroutine sayısını runtime.NumGoroutine() ile izlemek sorunu fark ettirdi.
Channel: goroutine’ler arası iletişim
Channel, goroutine’ler arasında veri iletmek için kullanılan Go’ya özgü bir yapıdır. “Belleği paylaşarak iletişim kur” yerine “iletişim kurarak bellek paylaş” felsefesi var burada.
package main
import "fmt"
func topla(sayilar []int, sonuc chan<- int) {
toplam := 0
for _, n := range sayilar {
toplam += n
}
sonuc <- toplam // Channel'a gönder
}
func main() {
sayilar := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sonuc := make(chan int)
// İki goroutine, listeyi ikiye bölerek topluyor
go topla(sayilar[:5], sonuc)
go topla(sayilar[5:], sonuc)
// İki sonucu al
a, b := <-sonuc, <-sonuc
fmt.Println("Toplam:", a+b) // 55
}
Channel’lar varsayılan olarak unbuffered’dır: gönderen taraf, alıcı okumadan bloklanır. Bu senkronizasyonu garanti eder.
Buffered channel ise belirli sayıda değeri alıcı olmadan tutabilir:
ch := make(chan int, 3) // 3 değer tutabilir
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 // Bu satır bloklardı — kanal dolu
Tamponlu kanal bir tür queue gibi davranıyor. Üretici-tüketici senaryolarında faydalı; ama tampon büyüklüğünü yanlış seçmek gizli tıkanmalara yol açabiliyor. Ben genelde önce tamponsuzu deniyor, gerçek bir performans sorunu görürsem tampon ekliyorum.
select: birden çok channel’ı dinlemek
select ifadesi, birden fazla channel’ı eş zamanlı dinlemeye yarıyor. Hangisi hazırsa oradan okuyor:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "bir"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "iki"
}()
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println("ch1'den:", msg)
case msg := <-ch2:
fmt.Println("ch2'den:", msg)
}
}
}
select bloğuna default case eklenebiliyor; o zaman hazır kanal yoksa bloklamak yerine default’a düşüyor. Bu, “varsa oku, yoksa geç” kalıbını uygulamak için kullanışlı.
PHP ile karşılaştırmak
PHP’de paralel iş yapmak için pcntl_fork(), Swoole gibi extension’lar ya da harici bir kuyruk sistemi gerekiyor. Bunların hiçbiri dil seviyesinde değil. Go’da goroutine ve channel, dilin kendisine yerleşik — standart kütüphane dışında hiçbir şey gereksiz.
Bu fark en çok şurada hissediliyor: Go’da eşzamanlı kod yazmak alışılmamış ama olağan. PHP’de eşzamanlılık, özel durumlara özgü hissettiriyor. Bir PHP dosyasını tarayıcı çağırdığında her istek kendi dünyasında çalışır, başkasının durumuna dokunmaz; bu izolasyon hem kolaylık hem de kısıtlamadır. Go’da ise birden fazla goroutine aynı belleğe erişebilir; bu güç ama dikkat gerektirir.
Hâlâ öğrenme sürecindeyim. Race condition, goroutine leak, channel’ların doğru kapatılması gibi konular dikkat gerektiriyor. Ama zihin modeli netleşti: goroutine ucuz, iletişim channel üzerinden, paylaşılan durum minimize edilmeli. Bu üç ilkeyi aklımda tuttuğumda kodun nereye gitmesi gerektiği çoğunlukla kendiliğinden ortaya çıkıyor.
Bir de concurrency ile parallelism ayrımını anlamak gerekiyor. Go’daki goroutine’ler eşzamanlı çalışır; aynı anda fiziksel olarak paralel çalışıp çalışmadıkları CPU çekirdek sayısına ve GOMAXPROCS ayarına bağlı. PHP’den gelince bu farkı görmek kolay değil — PHP’de “aynı anda birden fazla şey yap” kavramının karşılığı doğrudan süreç açmaktır. Go’da ise eşzamanlılık dil düzeyinde bir tasarım kararı; işi verimli sıralamak ile gerçekten paralel yürütmek ayrı katmanlar.