Goroutine başlatmak
Bir fonksiyon çağrısının önüne go koyun, o çağrı eşzamanlı çalışır. İfade hemen döner; çağıran beklemez.
Dört goroutine shout'u aynı anda çalıştırır. Bitme sıraları tanımlı değildir, ama çıktı her zaman orijinal sıradadır, çünkü her goroutine kendi indeksine yazar ve main slice'ı yalnızca wg.Wait() döndükten sonra okur.
Bu örnek zaten neredeyse her goroutine programının ihtiyaç duyduğu üç şeyi içeriyor: işi başlatmanın bir yolu (go), onu beklemenin bir yolu (sync.WaitGroup) ve iki goroutine aynı belleğe dokunmadan sonuçları geri almanın bir yolu (her birine bir slice elemanı).
main beklemez
main döndüğünde program çıkar. Hâlâ çalışan goroutine'ler oldukları yerde durdurulur. Hiçbir şey onları beklemez.
Bu genellikle yalnızca from main yazdırır. Bazen goroutine zamanında zamanlanır ve iki satırı da görürsünüz. Sorun o "genellikle"dir: sizin makinenizde çalışan ve yüklü bir sunucuda başarısız olan kod.
main'in sonundaki bir time.Sleep demonun iki satırı da yazdırmasını sağlar ve yanlış çözümdür. İşin ne kadar süreceğini tahmin eder. İşin kendisini bekleyin: bir WaitGroup ile (WaitGroup sayfası onu ayrıntılı anlatıyor) ya da bir channel ile.
Sonuçları geri almak
Bir go ifadesi fonksiyonun dönüş değerlerini atar. x := go f() derlenmez. Veri döndürmenin iki standart yolu vardır.
İlk örnekteki gibi goroutine başına bir yuva. Bir slice'ı önceden boyutlandırın, her goroutine'e indeksini verin, Wait'ten sonra okuyun. Sıra korunur ve kilit gerekmez, çünkü iki goroutine aynı elemana yazmaz.
Bir channel. Her goroutine sonucunu gönderir; alıcı onları toplar. Sonuçlar başlama sırasına göre değil, bitme sırasına göre gelir.
Tam olarak len(nums) değer almak bekleme işlevi de görür: her goroutine gönderene kadar main döngüyü geçemez. Geliş sırası çalıştırmalar arasında değişir, bu yüzden program sıraya bağlı herhangi bir şeyi yazdırmadan önce sıralar. Channels sayfası buffer'lı channel'ları, kapatmayı ve bir channel üzerinde range'i anlatıyor.
Döngü değişkenleri ve closure'lar (Go 1.22 değişikliği)
Go 1.22'den beri bir for döngüsünün her yinelemesi döngü değişkenlerinin yeni bir kopyasını alır. Bir goroutine içinde başlatılan bir closure o yinelemenin değerini yakalar, bu yüzden şu doğrudur:
for i, w := range words {
go func() {
results[i] = shout(w) // Go 1.22+: i and w belong to this iteration
}()
}
Go 1.22'den önce tüm yinelemeler tek bir i ve tek bir w paylaşıyordu ve her goroutine son değeri görme eğilimindeydi. Eski kod bunu değerleri argüman olarak aktararak, go func(i int, w string) { ... }(i, w), ya da gölgeleyerek, i := i, aşar. İkisi de Go 1.22 ve sonrasında zararsızdır ve var olan kodda hâlâ göreceksiniz. Yeni davranış, modülün go.mod dosyası go 1.22 ya da üstünü belirttiğinde geçerlidir.
Goroutine'ler ucuzdur
Bir goroutine, çalışma zamanının gerektiğinde büyütüp küçülttüğü küçük bir stack (birkaç kilobayt) ile başlar. Go zamanlayıcısı goroutine'leri bir işletim sistemi thread havuzu üzerinde çalıştırır; aynı anda Go kodu çalıştıran thread sayısı en fazla GOMAXPROCS'tur ve varsayılan olarak GOMAXPROCS CPU sayısına eşittir. Bir channel, bir mutex, bir sleep ya da ağ G/Ç'si üzerinde bloklanmak goroutine'i park eder ve thread'i bir başkası için serbest bırakır.
Bu yüzden büyük sayılarda bile görev başına bir goroutine başlatmak sorun değildir:
Yüz bin goroutine saniyenin küçük bir kesrinde biter. Toplam her zaman 4999950000'dir, çünkü atomic.Int64 her toplamayı bölünmez yapar. Yine de ucuz, bedava demek değildir: hâlâ bloklanmış her goroutine stack'ini ve başvurduğu her şeyi canlı tutar.
| İşletim sistemi thread'i | Goroutine | |
|---|---|---|
| Oluşturan | kernel | Go çalışma zamanı |
| İlk stack | sabit, çoğu zaman 1 MB ya da fazlası | birkaç KB, ihtiyaca göre büyür |
| Geçiş | kernel bağlam değişimi | Go zamanlayıcısı, kullanıcı alanında |
| Kimlik | bir thread ID'si var | tasarım gereği okuyabileceğiniz bir ID yok |
| Tipik sayı | yüzlerce | binlerden milyonlara |
Data race'ler
En az biri yazan iki goroutine'in aynı anda aynı değişkene erişmesi bir data race'tir. Sonuç yalnızca "biraz yanlış" değil, öngörülemezdir: güncellemeler kaybolur ve bir string, slice, map ya da interface değeri üzerindeki bir race programı çökertebilir ya da belleği bozabilir.
Çok çekirdekli bir makinede bu, çoğu çalıştırmada 10000'in altında farklı bir sayı yazdırır, çünkü iki goroutine aynı eski değeri okur ve ikisi de o değerin bir fazlasını geri yazar. Tek çekirdekte 10000 yazdırabilir, bu da daha kötüdür: hata testinizi geçer ve production'da ortaya çıkar.
Go bir race detector ile gelir. Programınızı ya da testlerinizi -race ile çalıştırın:
go run -race main.go
go test -race ./...
==================
WARNING: DATA RACE
Read at 0x00c000090038 by goroutine 8:
main.main.func1()
/tmp/race/main.go:16 +0x94
Previous write at 0x00c000090038 by goroutine 6:
main.main.func1()
/tmp/race/main.go:16 +0xa4
...
Found 2 data race(s)
exit status 66
Tam satırı (counter++) ve iki goroutine'i gösterir. Yalnızca çalıştırma sırasında gerçekten oluşan race'leri raporlar, bu yüzden onu eşzamanlı yolları çalıştıran testlerde kullanın. Programı birkaç kat yavaşlatır, bu yüzden production için değil, testler ve staging içindir.
En basitten en genele çözümler:
- Paylaşmayın. Her goroutine'e kendi verisini verin ve sonunda birleştirin (goroutine başına yuva kalıbı).
- Tek bir sayaç ya da bayrak için
sync/atomickullanın:var n atomic.Int64; n.Add(1). - Bir map ya da birkaç alanlı bir struct gibi daha büyük şeylerin etrafında bir
sync.Mutexkullanın. Mutex sayfasıRWMutexvesync.Once'ı da anlatıyor. - Veriyi bir channel üzerinden gönderin, böylece aynı anda yalnızca bir goroutine ona sahip olur.
Bir goroutine'deki panic programı öldürür
Bir goroutine panic olursa ve aynı goroutine'in içinde hiçbir şey recover etmezse, main ve diğer tüm goroutine'ler dahil bütün program çöker. main içindeki bir recover işe yaramaz, çünkü recover yalnızca kendi goroutine'indeki panic'leri yakalar.
Böyle recover etmek, tek bir hatalı isteğin geri kalanını çökertmemesi gereken uzun süre çalışan bir sunucunun kenarında anlamlıdır. Sıradan kodun içinde bir panic genellikle bir bug demektir ve yüksek sesle çökmek doğru sonuçtur.
Goroutine sızıntıları
Sonsuza kadar bloklanan bir goroutine hiç çıkmaz ve belleğini hiç serbest bırakmaz. Klasik neden, kimsenin asla almayacağı bir gönderimdir:
func firstResult(urls []string) string {
ch := make(chan string) // unbuffered
for _, u := range urls {
go func() { ch <- fetch(u) }()
}
return <-ch // takes the first result; the other senders block forever
}
Her çağrı len(urls) - 1 goroutine sızdırır. Bu isteği binlerce kez işleyen bir sunucuda bellek süreç ölene kadar tırmanır. İki çözüm: channel'ı her gönderenin bitirebileceği kadar büyük yapın (make(chan string, len(urls))) ya da goroutine'lere vazgeçmenin bir yolunu verin, genellikle bir context.Context artı ctx.Done() üzerinde bir select. Testlerde runtime.NumGoroutine() ile sızıntıları izleyebilirsiniz.
Aynı anda kaç tanesinin çalışacağını sınırlamak
"Öğe başına bir goroutine" 10.000 ucuz hesaplama için sorun değildir. Aynı sunucuya 10.000 HTTP isteği ya da 10.000 açık dosya için sorundur. Eşzamanlılığı semaphore olarak kullanılan buffer'lı bir channel ile sınırlayın:
Buffer'lı channel en fazla 3 token tutar, bu yüzden herhangi bir anda sem <- satırını geçmiş en fazla 3 goroutine olur. Tepe değer asla 3'ü aşamaz ve her biri uyuyan on iki görevle pratikte 3'e ulaşır. Bir jobs channel'ından okuyan sabit bir worker goroutine havuzu diğer yaygın biçimdir; WaitGroup sayfası bir tane kuruyor.
Standart kütüphanenin dışında, golang.org/x/sync/errgroup bir WaitGroup'u, ilk hatayı, context iptalini ve bir eşzamanlılık sınırını (g.SetLimit(n)) tek bir tipte birleştirir. Dördüne birden ihtiyaç duyan production kodunda olağan seçimdir.
Sık yapılan hatalar
- Beklemeyi unutmak.
maindöner ve iş sessizce hiç yapılmaz. Hergoifadesinin bittiğini bilmenin eşleşen bir yoluna ihtiyacı vardır. wg.Add'i goroutine'in içinde çağırmak.Wait,Add'den önce çalışıp sayacı sıfır görebilir ve erken dönebilir.Add'igoifadesinden önce çağırın.- Bir değişkeni senkronizasyon olmadan paylaşmak. Yaygın durum map'lerdir: bir map'e eşzamanlı yazmalar genellikle çalışma zamanı tarafından tespit edilir ve programı
recover'ın yakalayamadığıfatal error: concurrent map writesile çökertir. - Bir sıra varsaymak. Goroutine'ler zamanlayıcı hangi sırayı seçerse o sırayla çalışır. Çıktının sıralı olması gerekiyorsa toplayıp sıralayın ya da indeksli yuvalara yazın.
- Senkronizasyon için
time.Sleepkullanmak. Testleri yavaşlatır ve yine de kararsız bırakır. Bir tahmin üzerinde değil, olayın kendisi üzerinde bekleyin. - Durdurmanın bir yolu olmadan goroutine başlatmak. Döngüye giren ya da G/Ç bekleyen her şey, çağıranın onu iptal edebilmesi için bir
context.Contextalmalıdır.
Sıkça Sorulan Sorular
Go'da goroutine nedir?
Goroutine, programın geri kalanıyla eşzamanlı çalışan bir fonksiyon çağrısıdır. Bir çağrının önüne go koyarak başlatırsınız: go work(). Goroutine'leri işletim sistemi değil Go çalışma zamanı yönetir ve çalışma zamanı birçoğunu az sayıda işletim sistemi thread'i üzerine dağıtır, bu yüzden binlercesini başlatmak normaldir.
Go'da goroutine'lerin bitmesi nasıl beklenir?
Bir sync.WaitGroup kullanın: her go ifadesinden önce wg.Add(1), goroutine'in en üstünde defer wg.Done() ve hepsinin bitmesi gereken yerde wg.Wait() çağırın. Goroutine'ler değer üretiyorsa, her goroutine için bir channel'dan bir değer almak da bekleme işlevi görür.
Goroutine ile thread arasındaki fark nedir?
Bir işletim sistemi thread'inin sabit bir stack'i vardır (çoğu zaman 1 MB ya da daha fazla) ve kernel tarafından zamanlanır. Bir goroutine birkaç kilobaytlık, gerektiğinde büyüyen bir stack ile başlar ve Go zamanlayıcısı goroutine'ler arasında kullanıcı alanında geçiş yapar. Çalışma zamanı goroutine'leri aynı anda en fazla GOMAXPROCS thread üzerinde çalıştırır (varsayılan olarak CPU sayısı).
Bir goroutine'den dönüş değeri nasıl alınır?
Bir go ifadesi fonksiyonun dönüş değerlerini atar. Sonucu bir channel'a gönderin (results <- compute(x)) ya da önceden boyutlandırılmış bir slice'taki kendi yuvanıza yazın (out[i] = compute(x)) ve wg.Wait()'ten sonra okuyun.
Go programım goroutine bir şey yazdırmadan neden çıkıyor?
main döndüğünde program biter ve diğer tüm goroutine'ler kalan kodlarını çalıştırmadan durdurulur. Hiçbir şey goroutine'leri otomatik olarak beklemez. İş bitene kadar main'i bir WaitGroup ya da bir channel alımıyla bloklayın. time.Sleep eklemek yalnızca sorunu gizler.