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

https://pkg.go.dev/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

https://pkg.go.dev/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) x contains m and the argument list can be assigned to the parameter list of m. If x is addressable and &x’s method set contains m, x.m() is shorthand for (&x).m()