- Goroutine başlatmak ve
sync.WaitGroupile bitmelerini beklemek - Arabellekli ve arabelleksiz kanallarla,
close,rangeveselectile çalışmak - Ortak verileri
sync.Mutexile korumak ve kilitlenme ile sızıntı risklerini tanımak - İşçi havuzu (worker pool) kalıbını kurmak
Bir kafede tek bir barista varsa sıra yavaş ilerler: kahveler tek tek hazırlanır. Üç barista aynı anda çalıştığında siparişler çok daha çabuk hazır olur. Go programları da böyle çalışabilir: tek bir program binlerce görevi aynı anda yürütür. Go'yu bulut sunucuları için popüler yapan da bu basit eşzamanlılık modelidir: goroutine'ler, kanallar ve sync paketi.
Goroutine'ler
Bir goroutine, Go çalışma zamanının kendisi tarafından yönetilen hafif bir görevdir. Başlatmak için bir fonksiyon çağrısının önüne go yazmak yeterlidir: go work(). Bir goroutine yalnızca birkaç kilobaytlık bellekle başlar; bu yüzden binlerce, hatta milyonlarca goroutine oluşturulabilir. Önemli bir kural: main biter bitmez program da biter ve diğer goroutine'leri beklemez.
Eşzamanlılık (concurrency), birkaç görevin sırayla ilerlemesidir; programın nasıl yapılandırıldığıyla ilgilidir. Paralellik ise görevlerin gerçekten aynı anda, farklı işlemci çekirdeklerinde çalışmasıdır. Go eşzamanlı kod yazmanı sağlar ve birden çok çekirdek varsa onu otomatik olarak paralel çalıştırır.
Goroutine'lerin bitmesini beklemek için sync.WaitGroup kullanılır: Add(1) sayacı artırır, her goroutine sonunda Done() çağırır, Wait() ise sayaç sıfıra inene kadar bekler. Aşağıdaki örnekte her goroutine sonucunu dilimin kendi indeksine yazar; bu yüzden birbirlerine engel olmazlar ve yazdırma sırası hep aynıdır.
package main
import (
"fmt"
"sync"
)
func square(n int) int {
return n * n
}
func main() {
nums := []int{2, 3, 4, 5}
results := make([]int, len(nums))
var wg sync.WaitGroup
for i, n := range nums {
wg.Add(1)
go func() {
defer wg.Done()
results[i] = square(n)
}()
}
wg.Wait()
fmt.Println(results)
}[4 9 16 25]
Kanallar
Bir kanal, goroutine'ler arasında değer taşıyan bir borudur: ch := make(chan int). ch <- v bir değer gönderir, v := <-ch ise bir değer alır. Arabelleksiz bir kanalda gönderen, alıcı hazır olana kadar bekler; bu, goroutine'leri otomatik olarak eşitler. Gönderen işini bitirince close(ch) çağırır, alıcı ise kanal kapanana kadar for v := range ch ile okur. chan<- int yalnızca gönderme, <-chan int ise yalnızca alma kanalıdır.
package main
import "fmt"
func producer(ch chan<- int) {
for i := 1; i <= 3; i++ {
ch <- i * 10
}
close(ch)
}
func main() {
ch := make(chan int)
go producer(ch)
for v := range ch {
fmt.Println("received", v)
}
done := make(chan string)
go func() {
done <- "work finished"
}()
fmt.Println(<-done)
}received 10 received 20 received 30 work finished
Arabellekli bir kanalın (make(chan string, 3)) içinde yer vardır: arabellek dolana kadar gönderen beklemez. Bir kuyruk gibi çalışır: ilk giren ilk çıkar. len kanalda bekleyen eleman sayısını, cap ise arabellek boyutunu verir. Kapalı ve boş bir kanaldan okumak hemen sıfır değeri ve ok = false döndürür.
package main
import "fmt"
func main() {
queue := make(chan string, 3)
queue <- "Aysel"
queue <- "Murad"
fmt.Println(len(queue), cap(queue))
fmt.Println(<-queue)
queue <- "Leyla"
close(queue)
for name := range queue {
fmt.Println("next:", name)
}
v, ok := <-queue
fmt.Printf("%q %v\n", v, ok)
}2 3 Aysel next: Murad next: Leyla "" false
select ve zaman aşımları
select birkaç kanal işlemini aynı anda bekler ve hangisi önce hazır olursa onu çalıştırır; switch'e benzer ama kanallar içindir. time.After(d) belirli bir süre sonra değer veren bir kanal döndürür; bu yüzden onunla zaman aşımı kurmak kolaydır. Bir default kolu eklersen select hiç beklemez: hazır kanal yoksa hemen default çalışır.
package main
import (
"fmt"
"time"
)
func main() {
fast := make(chan string)
go func() {
time.Sleep(10 * time.Millisecond)
fast <- "result ready"
}()
select {
case msg := <-fast:
fmt.Println(msg)
case <-time.After(time.Second):
fmt.Println("timeout")
}
slow := make(chan string)
select {
case msg := <-slow:
fmt.Println(msg)
case <-time.After(50 * time.Millisecond):
fmt.Println("timeout")
}
}result ready timeout
select sonucu 10 ms sonra alır; ikinci kanala kimse göndermediği için 50 ms sonra zaman aşımı devreye girer.Mutex'ler ve işçi havuzu
Birkaç goroutine aynı değişkeni değiştirdiğinde bir veri yarışı (data race) oluşur: c.value++ aslında üç adımdır (oku, artır, yaz) ve goroutine'ler birbirinin sonucunun üzerine yazabilir. sync.Mutex bunu önler: Lock() ile Unlock() arasındaki kodu aynı anda yalnızca bir goroutine çalıştırabilir.
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func main() {
var c Counter
var wg sync.WaitGroup
for range 1000 {
wg.Add(1)
go func() {
defer wg.Done()
c.Inc()
}()
}
wg.Wait()
fmt.Println(c.value)
}1000
Gerçek projelerde binlerce görevin her biri için ayrı bir goroutine başlatmak yerine genellikle bir işçi havuzu (worker pool) kurulur: sabit sayıda işçi goroutine ortak bir jobs kanalından görev alır ve sonuçları bir results kanalına yazar. Böylece aynı anda çalışan goroutine sayısı kontrol altında kalır. Sonuçlar herhangi bir sırayla gelebilir; bu yüzden onları toplayıp sıralıyoruz.
package main
import (
"fmt"
"slices"
"sync"
)
func worker(jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for n := range jobs {
results <- n * n
}
}
func main() {
jobs := make(chan int, 10)
results := make(chan int, 10)
var wg sync.WaitGroup
for range 3 {
wg.Add(1)
go worker(jobs, results, &wg)
}
for n := 1; n <= 6; n++ {
jobs <- n
}
close(jobs)
wg.Wait()
close(results)
var squares []int
for r := range results {
squares = append(squares, r)
}
slices.Sort(squares)
fmt.Println(squares)
}[1 4 9 16 25 36]
Sıraya dikkat et: önce tüm görevler gönderilir ve işçilerin range döngüleri bitebilsin diye jobs kapatılır. Ardından wg.Wait() bütün işçileri bekler ve ancak bundan sonra results kapatılır. results kanalının arabelleği 10 olduğu için işçiler sonuç yazarken bloke olmaz.
Belleği paylaşarak iletişim kurma; iletişim kurarak belleği paylaş.
| Araç | Ne için |
|---|---|
go f() | bir görevi ayrı bir goroutine'de başlatmak |
sync.WaitGroup | bir grup goroutine'in bitmesini beklemek |
chan T | goroutine'ler arasında değer aktarmak |
select | birkaç kanalı beklemek, zaman aşımı koymak |
sync.Mutex | ortak bir değişkeni korumak |
Önemli noktalar
go f()hafif bir goroutine başlatır;mainbittiğinde bütün program biter.sync.WaitGroup(Add,Done,Wait) goroutine'lerin bitmesini bekler.- Kanal değer aktarır ve goroutine'leri eşitler; gönderen
closeçağırır, alıcırangeile okur. selectbirkaç kanalı bekler;time.Afterile zaman aşımı kurulur.- Ortak değişkenleri
sync.Mutexile koru ve kodu-raceile denetle; her goroutine'in bir bitiş yolu olmalıdır. - İşçi havuzu çok sayıda görevi sabit sayıda goroutine ile işler; sonuçları topla ve sabit bir sırayla yazdır.
Kendini test et
10 soru. Her doğru cevap XP kazandırır.
main, go work() çağırıp hemen biterse ne olur?