WaitGroup
ud-202
Aşağıdaki koda bir bakalım:
package main
import "fmt"
func bar() {
for i := 0; i < 5; i++ {
fmt.Println("bar(): ", i)
}
}
func foo() {
for i := 0; i < 5; i++ {
fmt.Println("foo(): ", i)
}
}
func main() {
foo()
bar()
}
Çıktı:
foo(): 0
foo(): 1
foo(): 2
foo(): 3
foo(): 4
bar(): 0
bar(): 1
bar(): 2
bar(): 3
bar(): 4
Beklediğimiz gibi aslında önce foo() sonra da bar() çalıştı.
package runtime
stdlib’te runtime isminde bir package bulunuyor. Ve burada bulunan çeşitli
fonksiyon ve sabitler var.
Kodumuzu buradan çeşitli bilgiler gösterecek şekilde değiştirelim:
package main
import (
"fmt"
"runtime"
)
func bar() {
for i := 0; i < 5; i++ {
fmt.Println("bar(): ", i)
}
}
func foo() {
for i := 0; i < 5; i++ {
fmt.Println("foo(): ", i)
}
}
func main() {
fmt.Println("OS:", runtime.GOOS)
fmt.Println("ARCH:", runtime.GOARCH)
fmt.Println("CPU:", runtime.NumCPU())
fmt.Println("Goroutines:", runtime.NumGoroutine())
foo()
bar()
}
Çıktı:
OS: linux
ARCH: amd64
CPU: 8
Goroutines: 1
foo(): 0
foo(): 1
foo(): 2
foo(): 3
foo(): 4
bar(): 0
bar(): 1
bar(): 2
bar(): 3
bar(): 4
Bu çıktıyı https://go.dev/play üzerinde elde ettim.
go
Şimdi go anahtar kelimesi ile foo() fonksiyonunu bir goroutine olarak
çalıştıralım.
package main
import (
"fmt"
"runtime"
)
func bar() {
for i := 0; i < 5; i++ {
fmt.Println("bar(): ", i)
}
}
func foo() {
for i := 0; i < 5; i++ {
fmt.Println("foo(): ", i)
}
}
func main() {
fmt.Println("OS:", runtime.GOOS)
fmt.Println("ARCH:", runtime.GOARCH)
fmt.Println("CPU:", runtime.NumCPU())
fmt.Println("Goroutines:", runtime.NumGoroutine())
go foo()
bar()
fmt.Println("Goroutines:", runtime.NumGoroutine())
}
Çıktı:
OS: linux
ARCH: amd64
CPU: 8
Goroutines: 1
bar(): 0
bar(): 1
bar(): 2
bar(): 3
bar(): 4
Goroutines: 2
İlk gözlemimiz şu: go foo() dedikten sonra goroutine sayımız 1’den 2’ye çıktı.
Fakat foo() nun çıktıları, print’leri nerede? Neden sadece bar() çıktıları
var. Buna geleceğiz ama kısaca şöyle. main() bittiği zaman programımızın akışı
sonlanıyor. Sequential çalışıyor, go foo() çalıştığı zaman yeni bir
goroutine başlıyor ve o da çalışıyor. foo() nun çıktıları gelene kadar ana
akış bitiyor ve sonlanıyor. Ama bar()ın loop iterasyon sayısını arttırırsak
o ara foo() yu görme ihtimalimiz artacak. Mesela bir çalıştırmada
bar(): 300
bar(): 301
foo(): 0
foo(): 1
foo(): 2
foo(): 3
foo(): 4
bar(): 302
bar(): 303
gördüm. Başkasında ise
bar(): 500
foo(): 0
foo(): 1
foo(): 2
foo(): 3
foo(): 4
bar(): 501
gördüm. Yani deterministik bir davranış bekleyemeyiz. Bu durumla nasıl başa çıkacağımızı konuşacağız.
Dikkat ederseniz sistemin 8 çekirdeği var yani concurrent kısımları parallel çalıştırabiliyor. Tek çekirdekli bir sistemde olsaydık farklı şeyler görebilirdik.
package sync
Bu paketin içerisinde goroutine’leri senkronlamak için kullanılabilecek çeşitli araçlar var, kendisi stdlib’in parçası.
Şimdi WaitGroup a bakalım.
https://pkg.go.dev/sync#WaitGroup
Aşağıdaki kodu inceleyelim.
1package main
2
3import (
4 "fmt"
5 "runtime"
6 "sync"
7)
8
9var wg sync.WaitGroup //package scope, sync paketinden geldi
10
11func bar() {
12 for i := 0; i < 5; i++ {
13 fmt.Println("bar(): ", i)
14 }
15}
16
17func foo() {
18 for i := 0; i < 5; i++ {
19 fmt.Println("foo(): ", i)
20 }
21 wg.Done()
22}
23
24func main() {
25 fmt.Println("OS:", runtime.GOOS)
26 fmt.Println("ARCH:", runtime.GOARCH)
27 fmt.Println("CPU:", runtime.NumCPU())
28 fmt.Println("Goroutines:", runtime.NumGoroutine())
29
30 wg.Add(1) // 1 şey için bekle
31
32 go foo()
33 bar()
34 fmt.Println("Goroutines:", runtime.NumGoroutine())
35 wg.Wait()
36}
Burada sync paketi içerisinden sync.WaitGroup türünde bir değişken
oluşturduk. Öncelikle wg.Add(1) ile tek bir şeyi beklediğimizi söylüyoruz ve
foo() içerisindeki wg.Done() o tek şey oluyor. Sonra da wg.Wait() ile
beklediğimiz her şeyin, bu durumda bizim için foo() nun işini bitirmesi
bitmesini bekliyoruz. Böylece programımız hemen sonlanmıyor, foo() nun
çıktılarını da görebiliyoruz.
Mesela:
OS: linux
ARCH: amd64
CPU: 8
Goroutines: 1
bar(): 0
bar(): 1
bar(): 2
bar(): 3
bar(): 4
Goroutines: 2
foo(): 0
foo(): 1
foo(): 2
foo(): 3
foo(): 4
Ama başka bir çalıştırmada aynı kod:
OS: linux
ARCH: amd64
CPU: 8
Goroutines: 1
bar(): 0
bar(): 1
bar(): 2
bar(): 3
bar(): 4
foo(): 0
foo(): 1
foo(): 2
foo(): 3
foo(): 4
Goroutines: 2
ya da
OS: linux
ARCH: amd64
CPU: 8
Goroutines: 1
bar(): 0
foo(): 0
foo(): 1
foo(): 2
foo(): 3
foo(): 4
bar(): 1
bar(): 2
bar(): 3
bar(): 4
Goroutines: 1
gibi bir çıktı verebiliyor. Yani apaçık bir şekilde race condition var, zaten olmayacağını garanti eden bir şey yapmadık.
Burada ilginç bir durum dikkatimizi çekebilir. Şuna bir bakalım:
type WaitGroup
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Go(f func())
func (wg *WaitGroup) Wait()
Kullandığımız fonksiyonların receiverları aslında pointer fakat biz
doğrudan değişken üzerinde wg.Add() gibi kullanabildik, bu nasıl oldu?
Burada pointer indirection özelliği var. Benim anladığım C’deki (*p).x
yerine p->x yazabiliyoruz ya işte burada bunu derleyici daha akıllıca yapıp
otomatik kullanmamıza izin veriyor gibi. [1]
A method call
x.m()is valid if the method set of (the type of)xcontainsmand the argument list can be assigned to the parameter list ofm. Ifxis addressable and&x’s method set containsm,x.m()is shorthand for(&x).m()