ProgramlamaBackend geliştirici

Gorutinler (goroutines) ve Go zamanlayıcısı nasıl çalışır ve görevlerin eşzamanlı yürütülmesi yönetiminde neden doğru bir şekilde yönetmek önemlidir?

Hintsage yapay zeka asistanı ile mülakatları geçin

Cevap.

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:

  • Gorutinlerin anlık ve ucuz bir şekilde oluşturulması (sistem iş parçacığından on binlerce kat daha ucuz).
  • Kanallar aracılığıyla doğrudan etkileşim, senkronizasyon ve veri paylaşımı sağlanması.
  • İşlemlerin sonlandırılması için manuel kontrol gerekliliği (hangi gorutinlerin bekletildiği, kimlerin durdurduğu, durdurma sinyalinin nasıl verileceği).

Şıkkı olan sorular.

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 } }

Yaygın hatalar ve anti-patarnlar

  • Gorutinlerin tamamlanmasını kontrol etmeden başlatılması
  • Değişkenlerin döngüde yakalanması, anonim işlevlerin içine aktarılmadan
  • "Sızan gorutinler" nedeniyle sistemin aşırı yüklenmesi
  • Kanallar üzerinden veri aktarımında senkronizasyon hatalarının göz ardı edilmesi

Hayattan örnek

Negatif durum

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:

  • Yüksek başlatma hızı
  • Ölçekleme kolaylığı

Eksiler:

  • Bellek sızıntısı
  • Yanıt süresinin artması
  • Öngörülemeyen tamamlama

Pozitif durum

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:

  • Öngörülebilir yaşam döngüsü
  • Sonlandırma yönetimi
  • Kolayca ölçeklenebilir

Eksiler:

  • İptal ve senkronizasyon mantığını açıkça yazmak zorundasınız
  • Programın mimarisi biraz daha karmaşık