Gorutinler, Go'nun etkili eşzamanlılık sağlamak için ilk sürümlerinde yerleştirilen hafif iş parçacıklarıdır. Tarihsel olarak hafif iş parçacığı fikri, sistem iş parçacıklarının maliyetini aşma ve ölçeklenebilir sunucu uygulamalarına duyulan yüksek ihtiyaç nedeniyle ortaya çıkmıştır. Go, öncelikle paralel olarak bir milyondan fazla görevin işlenmesi gereken sunucu ve ağ sistemleri için bir dil olarak tasarlanmıştır.
Sorun: Eşzamanlılık, gorutinlerin yaşam döngüsünü kontrol edilmezse, planlama dikkate alınmazsa ve işleme sonu yönetilmezse, yarış koşullarına, kilitlenmelere ve bellek tüketiminin artmasına neden olabilir.
Çözüm: Gorutinler go anahtar kelimesi ile başlatılır. Gorutinlerin çalışması Go zamanlayıcısı tarafından planlanır ve bu zamanlayıcı M:N modelini kullanır (M sistem iş parçacığı N Go dili gorutini hizmet eder). Yaşam döngüsünü yönetmek için kanallar, WaitGroup, context ve kanal kapama kontrolü kullanılır.
Kod örneği:
package main import ("fmt"; "time") func worker(id int) { fmt.Printf("Worker %d started ", id) time.Sleep(time.Second) fmt.Printf("Worker %d done ", id) } func main() { for i := 1; i <= 3; i++ { go worker(i) } time.Sleep(2 * time.Second) }
Anahtar özellikler:
Main içerisinde gorutini açıkça beklemezsek, her zaman çalışır mı?
Hayır, main tamamlandığında — proses tamamlanır ve alt gorutinlerin durumu ne olursa olsun, her görev yerine getirilmeyecek.
go func(...)'i döngüden başlatmak, her gorutinin döngü değişkenlerinin kendi değerini alacağı garantisi midir?
Hayır, döngü değişkeninin yakalanması sorunu ortaya çıkar, gorutinler aynı dilim/değişken değeriyle çalışabilir. Değişkenin kopyasını kullanmak gerekir, örneğin argüman olarak geçirmek:
for i := 0; i < 3; i++ { go func(n int) { fmt.Println(n) }(i) }
Bir gorutin Go zamanlayıcısını engelleyebilir ve diğerlerinin çalışmasını engelleyebilir mi?
Evet, eğer içinde sonsuz veya çok ağır bir döngü varsa ve geçiş noktaları yoksa (örneğin, zamanlama işlevi veya yield çağrıları olmadan), işletim sistemi iş parçacığını tutabilir — bu da Go'nun "kooperatif çoklu görev" ideolojisiyle çelişir. Örneğin, kilitlenme olmayan ağır bir işlev:
func busy() { for { // Bekleyiş ya da engelleme çağrıları yok } }
Bir mikroserviste belirli aralıklarla veritabanından okuma yapan bir gorutin başlatılır ama isteğin iptalinde sonlandırmayı unutur. Sonuçta, zamanla tüm bellek tüketimine neden olan "askıda kalan" gorutinler kalır.
Artılar:
Eksiler:
Görevlerin iptali için bir context kullanılır, WaitGroup — uygulama durdurulmadan önce tüm gorutinlerin sonlandırılması için, ve kanallar — yürütücüler arasında verilerin düzgün iletimi için kullanılır.
Artılar:
Eksiler: