Go Eşzamanlılık Başlangıç Paketi
Genel Bakış
Kısa Tanıtım
Go dilinde eşzamanlılık yönetimi için birçok araç bulunmaktadır. Bu makalede, bunlardan bazılarını ve bazı yöntemleri sizlere tanıtacağız.
Goroutine Nedir?
goroutine, Go dilinde desteklenen yeni bir eşzamanlılık modeli türüdür. Genellikle programlar, aynı anda birden fazla görevi yerine getirmek için işletim sisteminden OS thread'leri alarak, çekirdek sayısı kadar paralel işlem gerçekleştirir. Daha küçük birimlerde eşzamanlılık sağlamak için ise userland'de green thread'ler oluşturulur ve tek bir OS thread içinde birden fazla green thread'in çalışması sağlanır. Ancak goroutine'ler, bu tür green thread'leri daha küçük ve verimli hale getirmiştir. Bu goroutine'ler, thread'lerden daha az bellek kullanır ve thread'lerden daha hızlı oluşturulup değiştirilebilir.
Goroutine kullanmak için yalnızca go anahtar kelimesini kullanmak yeterlidir. Bu, program yazma sürecinde senkron kodu sezgisel olarak asenkron kod olarak çalıştırmayı sağlar.
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 ch := make(chan struct{})
10 go func() {
11 defer close(ch) // ch'yi kapatmayı ertele
12 time.Sleep(1 * time.Second) // 1 saniye bekle
13 fmt.Println("Hello, World!") // "Hello, World!" yazdır
14 }()
15
16 fmt.Println("Waiting for goroutine...") // "Goroutine bekleniyor..." yazdır
17 for range ch {} // ch kapanana kadar bekle
18}
Bu kod, basitçe 1 saniye bekleyip Hello, World! çıktısını veren senkron bir kodu asenkron bir akışa dönüştürmektedir. Mevcut örnek basit olsa da, biraz karmaşık bir kodu senkron koddan asenkron koda dönüştürdüğümüzde, kodun okunabilirliği, görünürlüğü ve anlaşılırlığı async await veya promise gibi geleneksel yöntemlere göre daha iyi hale gelmektedir.
Ancak çoğu durumda, bu tür senkron kodları sadece asenkron olarak çağırma akışını ve fork & join gibi akışları (tıpkı böl ve yönet benzeri bir akış) anlamadan kötü goroutine kodları üretilebilmektedir. Bu makalede, bu tür durumlara hazırlıklı olmak için kullanılabilecek bazı yöntemleri ve teknikleri tanıtacağız.
Eşzamanlılık Yönetimi
context
İlk yönetim tekniği olarak contextin ortaya çıkması şaşırtıcı olabilir. Ancak Go dilinde context, basit bir iptal işlevinin ötesinde, tüm görev ağacını yönetmede üstün bir rol oynar. Bilmeyenler için bu paketi kısaca açıklayalım.
1package main
2
3func main() {
4 ctx, cancel := context.WithCancel(context.Background())
5 defer cancel() // cancel fonksiyonunu ertele
6
7 go func() {
8 <-ctx.Done() // ctx.Done() kanalından veri gelene kadar bekle
9 fmt.Println("Context is done!") // "Context tamamlandı!" yazdır
10 }()
11
12 time.Sleep(1 * time.Second) // 1 saniye bekle
13
14 cancel() // İptal et
15
16 time.Sleep(1 * time.Second) // 1 saniye bekle
17}
Yukarıdaki kod, context kullanarak 1 saniye sonra Context is done! çıktısını veren bir koddur. context, Done() metodu aracılığıyla iptal durumunu kontrol edebilir ve WithCancel, WithTimeout, WithDeadline, WithValue gibi metodlar aracılığıyla çeşitli iptal yöntemleri sunar.
Basit bir örnek oluşturalım. Diyelim ki, bazı verileri almak için aggregator pattern'ini kullanarak user, post ve comment verilerini getiren bir kod yazmanız gerekiyor. Ve tüm isteklerin 2 saniye içinde tamamlanması gerekiyorsa, bunu aşağıdaki gibi yazabilirsiniz.
1package main
2
3func main() {
4 ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second) // 2 saniyelik zaman aşımı ile context oluştur
5 defer cancel() // cancel fonksiyonunu ertele
6
7 ch := make(chan struct{}) // struct tipinde bir kanal oluştur
8 go func() {
9 defer close(ch) // ch'yi kapatmayı ertele
10 user := getUser(ctx) // Kullanıcı verisini al
11 post := getPost(ctx) // Post verisini al
12 comment := getComment(ctx) // Yorum verisini al
13
14 fmt.Println(user, post, comment) // Kullanıcı, post ve yorum verilerini yazdır
15 }()
16
17 select {
18 case <-ctx.Done(): // Context tamamlandıysa
19 fmt.Println("Timeout!") // "Zaman Aşımı!" yazdır
20 case <-ch: // ch'den veri geldiyse
21 fmt.Println("All data is fetched!") // "Tüm veriler alındı!" yazdır
22 }
23}
Yukarıdaki kod, 2 saniye içinde tüm verileri alamazsa Timeout! çıktısını verir, tüm verileri alırsa All data is fetched! çıktısını verir. Bu şekilde context kullanarak, birden fazla goroutine'in çalıştığı kodlarda bile iptal ve zaman aşımını kolayca yönetebilirsiniz.
Bu konuyla ilgili çeşitli context fonksiyonları ve metodları godoc context adresinden kontrol edilebilir. Basit olanları öğrenip rahatça kullanabilmenizi dileriz.
channel
unbuffered channel
channel, goroutine'ler arasında iletişim kurmak için bir araçtır. channel, make(chan T) ile oluşturulabilir. Burada T, bu channel'ın ileteceği verinin tipidir. channel, <- operatörü ile veri alıp gönderebilir ve close fonksiyonu ile channel kapatılabilir.
1package main
2
3func main() {
4 ch := make(chan int) // int tipinde bir kanal oluştur
5 go func() {
6 ch <- 1 // Kanala 1 gönder
7 ch <- 2 // Kanala 2 gönder
8 close(ch) // Kanalı kapat
9 }()
10
11 for i := range ch { // Kanaldan gelen verileri i'ye ata
12 fmt.Println(i) // i'yi yazdır
13 }
14}
Yukarıdaki kod, channel kullanarak 1 ve 2'yi çıktı veren bir koddur. Bu kodda, sadece channel'a değer gönderip almak gösterilmektedir. Ancak channel, bundan daha fazla özellik sunmaktadır. Öncelikle buffered channel ve unbuffered channel hakkında bilgi edinelim. Başlamadan önce, yukarıda yazılan örnek unbuffered channel olup, kanala veri gönderme eylemi ile veri alma eyleminin aynı anda gerçekleşmesi gerekmektedir. Eğer bu eylemler aynı anda gerçekleşmezse, deadlock oluşabilir.
buffered channel
Eğer yukarıdaki kod basit bir çıktı değil de, iki ağır iş yapan birer işlem olsaydı ne olurdu? İkinci işlem okuma ve işleme sırasında uzun süre takılırsa, ilk işlem de o süre boyunca duracaktır. Bu durumu önlemek için buffered channel kullanabiliriz.
1package main
2
3func main() {
4 ch := make(chan int, 2) // 2 kapasiteli int tipinde bir kanal oluştur
5 go func() {
6 ch <- 1 // Kanala 1 gönder
7 ch <- 2 // Kanala 2 gönder
8 close(ch) // Kanalı kapat
9 }()
10
11 for i := range ch { // Kanaldan gelen verileri i'ye ata
12 fmt.Println(i) // i'yi yazdır
13 }
14}
Yukarıdaki kod, buffered channel kullanarak 1 ve 2'yi çıktı veren bir koddur. Bu kodda, buffered channel kullanılarak, channel'a veri gönderme eylemi ile veri alma eyleminin aynı anda gerçekleşmesi gerekmeyecek şekilde tasarlanmıştır. Bu şekilde kanala bir buffer eklenerek, belirtilen uzunluk kadar bir boşluk oluşur ve sonraki işlemlerin gecikmesinden kaynaklanan iş gecikmeleri önlenir.
select
Birden fazla kanalı işlerken, select ifadesini kullanarak fan-in yapısını kolayca uygulayabilirsiniz.
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 ch1 := make(chan int, 10) // 10 kapasiteli int tipinde bir kanal oluştur
10 ch2 := make(chan int, 10) // 10 kapasiteli int tipinde bir kanal oluştur
11 ch3 := make(chan int, 10) // 10 kapasiteli int tipinde bir kanal oluştur
12
13 go func() {
14 for {
15 ch1 <- 1 // ch1'e 1 gönder
16 time.Sleep(1 * time.Second) // 1 saniye bekle
17 }
18 }()
19 go func() {
20 for {
21 ch2 <- 2 // ch2'ye 2 gönder
22 time.Sleep(2 * time.Second) // 2 saniye bekle
23 }
24 }()
25 go func() {
26 for {
27 ch3 <- 3 // ch3'e 3 gönder
28 time.Sleep(3 * time.Second) // 3 saniye bekle
29 }
30 }()
31
32 for i := 0; i < 3; i++ { // 3 kez döngü yap
33 select {
34 case v := <-ch1: // ch1'den veri geldiyse
35 fmt.Println(v) // v'yi yazdır
36 case v := <-ch2: // ch2'den veri geldiyse
37 fmt.Println(v) // v'yi yazdır
38 case v := <-ch3: // ch3'ten veri geldiyse
39 fmt.Println(v) // v'yi yazdır
40 }
41 }
42}
Yukarıdaki kod, periyodik olarak 1, 2, 3 gönderen 3 kanal oluşturur ve select kullanarak kanallardan değer alıp çıktı veren bir koddur. Bu şekilde select kullanarak, birden fazla kanaldan aynı anda veri alabilir ve kanaldan değer alındığı anda işleyebilirsiniz.
for range
channel, for range kullanılarak kolayca veri alabilir. for range bir kanalda kullanıldığında, o kanala veri eklendiğinde çalışır ve kanal kapatıldığında döngüyü sonlandırır.
1package main
2
3func main() {
4 ch := make(chan int) // int tipinde bir kanal oluştur
5 go func() {
6 ch <- 1 // Kanala 1 gönder
7 ch <- 2 // Kanala 2 gönder
8 close(ch) // Kanalı kapat
9 }()
10
11 for i := range ch { // Kanaldan gelen verileri i'ye ata
12 fmt.Println(i) // i'yi yazdır
13 }
14}
Yukarıdaki kod, channel kullanarak 1 ve 2'yi çıktı veren bir koddur. Bu kodda, for range kullanılarak kanala veri eklendiğinde veri alınıp çıktı verilmektedir. Ve kanal kapatıldığında döngü sonlandırılır.
Yukarıda birkaç kez belirtildiği gibi, bu sözdizimi basit bir senkronizasyon aracı olarak da kullanılabilir.
1package main
2
3func main() {
4 ch := make(chan struct{}) // struct tipinde bir kanal oluştur
5 go func() {
6 defer close(ch) // ch'yi kapatmayı ertele
7 time.Sleep(1 * time.Second) // 1 saniye bekle
8 fmt.Println("Hello, World!") // "Hello, World!" yazdır
9 }()
10
11 fmt.Println("Waiting for goroutine...") // "Goroutine bekleniyor..." yazdır
12 for range ch {} // ch kapanana kadar bekle
13}
Yukarıdaki kod, 1 saniye bekleyip Hello, World! çıktısını veren bir koddur. Bu kodda, channel kullanılarak senkron kod asenkron koda dönüştürülmüştür. Bu şekilde channel kullanarak, senkron kodları kolayca asenkron kodlara dönüştürebilir ve join noktaları belirleyebilirsiniz.
vb.
- nil channel'a veri gönderilir veya nil channel'dan veri alınırsa, sonsuz döngüye girilerek deadlock oluşabilir.
- Kanal kapatıldıktan sonra veri gönderilirse, panic oluşur.
- Kanalı kapatmaya gerek yoktur, GC toplama işlemi sırasında kanalı kapatır.
mutex
spinlock
spinlock, bir döngüde sürekli olarak kilit denemesi yapan bir senkronizasyon yöntemidir. Go dilinde, işaretçiler kullanılarak spinlock kolayca uygulanabilir.
1package spinlock
2
3import (
4 "runtime"
5 "sync/atomic"
6)
7
8type SpinLock struct {
9 lock uintptr
10}
11
12func (s *SpinLock) Lock() {
13 for !atomic.CompareAndSwapUintptr(&s.lock, 0, 1) { // Eğer s.lock 0 ise 1 ile değiştir, başarılı değilse dön
14 runtime.Gosched() // CPU'yu başka goroutine'e bırak
15 }
16}
17
18func (s *SpinLock) Unlock() {
19 atomic.StoreUintptr(&s.lock, 0) // s.lock'ı 0 olarak ayarla
20}
21
22func NewSpinLock() *SpinLock {
23 return &SpinLock{} // Yeni bir SpinLock döndür
24}
Yukarıdaki kod, spinlock paketini uygulayan bir koddur. Bu kodda, sync/atomic paketi kullanılarak SpinLock implemente edilmiştir. Lock metodunda atomic.CompareAndSwapUintptr kullanılarak kilit denemesi yapılırken, Unlock metodunda atomic.StoreUintptr kullanılarak kilit serbest bırakılır. Bu yöntem, kilit almak için durmaksızın deneme yaptığı için, kilit elde edilene kadar sürekli CPU kullanır ve sonsuz döngüye girebilir. Bu nedenle, spinlock basit senkronizasyonlarda veya kısa süreli kullanımlarda tercih edilmelidir.
sync.Mutex
mutex, goroutine'ler arasında senkronizasyon için bir araçtır. sync paketi tarafından sağlanan mutex, Lock, Unlock, RLock, RUnlock gibi metodlar sunar. mutex, sync.Mutex ile oluşturulabilir ve sync.RWMutex ile okuma/yazma kilidi de kullanılabilir.
1package main
2
3import (
4 "sync"
5)
6
7func main() {
8 var mu sync.Mutex // Mutex değişkeni oluştur
9 var count int // Sayı değişkeni oluştur
10
11 go func() {
12 mu.Lock() // Kilitle
13 count++ // Sayıyı artır
14 mu.Unlock() // Kilidi aç
15 }()
16
17 mu.Lock() // Kilitle
18 count++ // Sayıyı artır
19 mu.Unlock() // Kilidi aç
20
21 println(count) // Sayıyı yazdır
22}
Yukarıdaki kodda, neredeyse aynı anda iki goroutine aynı count değişkenine erişecektir. Bu durumda, mutex kullanarak count değişkenine erişen kodu kritik bölge haline getirirsek, count değişkenine eşzamanlı erişimi engelleyebiliriz. Böylece, bu kod kaç kez çalıştırılırsa çalıştırılsın, her zaman 2 çıktısını verecektir.
sync.RWMutex
sync.RWMutex, okuma kilidi ve yazma kilidini ayrı ayrı kullanabilen bir mutex türüdür. Okuma kilidini almak ve bırakmak için RLock ve RUnlock metodları kullanılır.
1package cmap
2
3import (
4 "sync"
5)
6
7type ConcurrentMap[K comparable, V any] struct {
8 sync.RWMutex // Okuma/yazma mutex'i
9 data map[K]V // Veri haritası
10}
11
12func (m *ConcurrentMap[K, V]) Get(key K) (V, bool) {
13 m.RLock() // Okuma kilidi al
14 defer m.RUnlock() // Okuma kilidini bırakmayı ertele
15
16 value, ok := m.data[key] // Veriyi al
17 return value, ok // Veriyi ve durumu döndür
18}
19
20func (m *ConcurrentMap[K, V]) Set(key K, value V) {
21 m.Lock() // Yazma kilidi al
22 defer m.Unlock() // Yazma kilidini bırakmayı ertele
23
24 m.data[key] = value // Veriyi ayarla
25}
Yukarıdaki kod, sync.RWMutex kullanarak ConcurrentMap'i uygulayan bir koddur. Bu kodda, Get metodunda okuma kilidi alınırken, Set metodunda yazma kilidi alınarak data haritasına güvenli bir şekilde erişilebilir ve değiştirilebilir. Okuma kilidinin gerekli olmasının nedeni, basit okuma işlemlerinin çok olduğu durumlarda, yazma kilidi almadan sadece okuma kilidi alarak birden fazla goroutine'in aynı anda okuma işlemlerini gerçekleştirmesine izin vermektir. Bu sayede, durum değişikliği gerektirmeyen ve yazma kilidi alınmasına gerek olmayan durumlarda sadece okuma kilidi alarak performans artırılabilir.
fakelock
fakelock, sync.Locker arayüzünü uygulayan basit bir yöntemdir. Bu yapı, sync.Mutex ile aynı metodları sağlar ancak gerçekte herhangi bir işlem yapmaz.
1package fakelock
2
3type FakeLock struct{} // FakeLock yapısı
4
5func (f *FakeLock) Lock() {} // Lock metodu, hiçbir şey yapmaz
6
7func (f *FakeLock) Unlock() {} // Unlock metodu, hiçbir şey yapmaz
Yukarıdaki kod, fakelock paketini uygulayan bir koddur. Bu paket, sync.Locker arayüzünü uygulayarak Lock ve Unlock metodlarını sağlar, ancak gerçekte hiçbir işlem yapmaz. Bu tür bir kodun neden gerekli olduğu fırsat bulduğumda açıklayacağım.
waitgroup
sync.WaitGroup
sync.WaitGroup, tüm goroutine'lerin işleri bitene kadar beklemek için kullanılan bir araçtır. Add, Done, Wait metodlarını sunar; Add metodu ile goroutine sayısı eklenir, Done metodu ile goroutine'in işinin bittiği bildirilir. Ve Wait metodu ile tüm goroutine'lerin işleri bitene kadar beklenir.
1package main
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8func main() {
9 wg := sync.WaitGroup{} // WaitGroup oluştur
10 c := atomic.Int64{} // Atomik int64 oluştur
11
12 for i := 0; i < 100 ; i++ { // 100 kez döngü yap
13 wg.Add(1) // WaitGroup sayacını artır
14 go func() {
15 defer wg.Done() // Fonksiyon bitince Done çağır
16 c.Add(1) // c'ye 1 ekle
17 }()
18 }
19
20 wg.Wait() // Tüm goroutine'lerin bitmesini bekle
21 println(c.Load()) // c'nin değerini yazdır
22}
Yukarıdaki kod, sync.WaitGroup kullanarak 100 goroutine'in aynı anda c değişkenine değer eklediği bir koddur. Bu kodda, sync.WaitGroup kullanılarak tüm goroutine'ler bitene kadar beklenir ve ardından c değişkenine eklenen değer çıktı olarak verilir. Sadece birkaç görevi fork & join yapmak için sadece kanal kullanmak yeterli olsa da, çok sayıda görevi fork & join yapmak için sync.WaitGroup kullanmak da iyi bir seçenektir.
with slice
Slice ile birlikte kullanıldığında, waitgroup kilit olmadan mükemmel eşzamanlı çalışma yönetimi aracı olabilir.
1package main
2
3import (
4 "fmt"
5 "sync"
6 "rand" // rand paketi eksik, "math/rand" olmalı
7)
8
9func main() {
10 var wg sync.WaitGroup // WaitGroup oluştur
11 arr := [10]int{} // 10 elemanlı int dizisi oluştur
12
13 for i := 0; i < 10; i++ { // 10 kez döngü yap
14 wg.Add(1) // WaitGroup sayacını artır
15 go func(id int) {
16 defer wg.Done() // Fonksiyon bitince Done çağır
17
18 arr[id] = rand.Intn(100) // id indeksine rastgele sayı ata
19 }(i)
20 }
21
22 wg.Wait() // Tüm goroutine'lerin bitmesini bekle
23 fmt.Println("Done") // "Tamamlandı" yazdır
24
25 for i, v := range arr { // Dizideki her eleman için
26 fmt.Printf("arr[%d] = %d\n", i, v) // Dizinin elemanını yazdır
27 }
28}
Yukarıdaki kod, yalnızca waitgroup kullanarak her goroutine'in aynı anda 10 rastgele tam sayı oluşturup atanmış indeksine kaydettiği bir koddur. Bu kodda, waitgroup kullanılarak tüm goroutine'ler bitene kadar beklenir ve ardından Done çıktısı verilir. Bu şekilde waitgroup kullanarak, birden fazla goroutine'in aynı anda görevleri gerçekleştirmesini sağlayabilir, tüm goroutine'ler bitene kadar kilit olmadan verileri saklayabilir ve görev tamamlandıktan sonra toplu olarak işlem yapabilirsiniz.
golang.org/x/sync/errgroup.ErrGroup
errgroup, sync.WaitGroup'un bir uzantısı olan bir pakettir. errgroup, sync.WaitGroup'tan farklı olarak, goroutine'lerden herhangi birinde bir hata meydana gelirse, tüm goroutine'leri iptal eder ve hatayı döndürür.
1package main
2
3import (
4 "context"
5 "fmt"
6 "golang.org/x/sync/errgroup"
7)
8
9func main() {
10 g, ctx := errgroup.WithContext(context.Background()) // errgroup ve context oluştur
11 _ = ctx // ctx değişkenini kullanmadığımızı belirtir
12
13 for i := 0; i < 10; i++ { // 10 kez döngü yap
14 i := i // i'nin kopyasını oluştur
15 g.Go(func() error { // Bir goroutine başlat
16 if i == 5 { // Eğer i 5 ise
17 return fmt.Errorf("error") // Hata döndür
18 }
19 return nil // Hata yoksa nil döndür
20 })
21 }
22
23 if err := g.Wait(); err != nil { // Tüm goroutine'lerin bitmesini bekle ve hata varsa
24 fmt.Println(err) // Hatayı yazdır
25 }
26}
Yukarıdaki kod, errgroup kullanarak 10 goroutine oluşturur ve 5. goroutine'de bir hata oluşturan bir koddur. Bilerek beşinci goroutine'de bir hata oluşturdum ki, hata durumlarını gösterebileyim. Ancak gerçek kullanımda, errgroup kullanarak goroutine'ler oluşturulur ve her goroutine'de meydana gelen hatalara karşı çeşitli son işlemler yapılarak kullanılır.
once
Sadece bir kez çalışması gereken kodu çalıştıran bir araçtır. Aşağıdaki yapıcılar aracılığıyla ilgili kod çalıştırılabilir.
1func OnceFunc(f func()) func()
2func OnceValue[T any](f func() T) func() T
3func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)
OnceFunc
OnceFunc, ilgili fonksiyonun tüm süreç boyunca sadece bir kez çalıştırılmasını sağlar.
1package main
2
3import "sync"
4
5func main() {
6 once := sync.OnceFunc(func() { // Bir kez çalışacak fonksiyon oluştur
7 println("Hello, World!") // "Hello, World!" yazdır
8 })
9
10 once() // Bir kez çalıştır
11 once() // Bir kez çalıştır
12 once() // Bir kez çalıştır
13 once() // Bir kez çalıştır
14 once() // Bir kez çalıştır
15}
Yukarıdaki kod, sync.OnceFunc kullanarak Hello, World! çıktısını veren bir koddur. Bu kodda, sync.OnceFunc kullanılarak once fonksiyonu oluşturulur ve once fonksiyonu birden fazla kez çağrılsa bile Hello, World! yalnızca bir kez çıktı verir.
OnceValue
OnceValue, fonksiyonun sadece bir kez çalıştırılmasını sağlamakla kalmaz, aynı zamanda fonksiyonun dönüş değerini saklayarak tekrar çağrıldığında saklanan değeri döndürür.
1package main
2
3import "sync"
4
5func main() {
6 c := 0 // c değişkenini 0 olarak başlat
7 once := sync.OnceValue(func() int { // Bir kez çalışacak ve değer döndürecek fonksiyon oluştur
8 c += 1 // c'yi artır
9 return c // c'yi döndür
10 })
11
12 println(once()) // once fonksiyonunu çağır ve sonucu yazdır
13 println(once()) // once fonksiyonunu çağır ve sonucu yazdır
14 println(once()) // once fonksiyonunu çağır ve sonucu yazdır
15 println(once()) // once fonksiyonunu çağır ve sonucu yazdır
16 println(once()) // once fonksiyonunu çağır ve sonucu yazdır
17}
Yukarıdaki kod, sync.OnceValue kullanarak c değişkenini 1'er 1'er artıran bir koddur. Bu kodda, sync.OnceValue kullanılarak once fonksiyonu oluşturulur ve once fonksiyonu birden fazla kez çağrılsa bile c değişkeni yalnızca bir kez artırılmış olan 1 değerini döndürür.
OnceValues
OnceValues, OnceValue ile aynı şekilde çalışır ancak birden fazla değer döndürebilir.
1package main
2
3import "sync"
4
5func main() {
6 c := 0 // c değişkenini 0 olarak başlat
7 once := sync.OnceValues(func() (int, int) { // Bir kez çalışacak ve iki değer döndürecek fonksiyon oluştur
8 c += 1 // c'yi artır
9 return c, c // c ve c'yi döndür
10 })
11
12 a, b := once() // once fonksiyonunu çağır ve a, b'ye ata
13 println(a, b) // a ve b'yi yazdır
14 a, b = once() // once fonksiyonunu çağır ve a, b'ye ata
15 println(a, b) // a ve b'yi yazdır
16 a, b = once() // once fonksiyonunu çağır ve a, b'ye ata
17 println(a, b) // a ve b'yi yazdır
18 a, b = once() // once fonksiyonunu çağır ve a, b'ye ata
19 println(a, b) // a ve b'yi yazdır
20 a, b = once() // once fonksiyonunu çağır ve a, b'ye ata
21 println(a, b) // a ve b'yi yazdır
22}
Yukarıdaki kod, sync.OnceValues kullanarak c değişkenini 1'er 1'er artıran bir koddur. Bu kodda, sync.OnceValues kullanılarak once fonksiyonu oluşturulur ve once fonksiyonu birden fazla kez çağrılsa bile c değişkeni yalnızca bir kez artırılmış olan 1 değerini döndürür.
atomic
atomic paketi, atomik işlemler sağlayan bir pakettir. atomic paketi Add, CompareAndSwap, Load, Store, Swap gibi metodlar sunsa da, son zamanlarda Int64, Uint64, Pointer gibi tiplerin kullanılması tavsiye edilmektedir.
1package main
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8func main() {
9 wg := sync.WaitGroup{} // WaitGroup oluştur
10 c := atomic.Int64{} // Atomik int64 oluştur
11
12 for i := 0; i < 100 ; i++ { // 100 kez döngü yap
13 wg.Add(1) // WaitGroup sayacını artır
14 go func() {
15 defer wg.Done() // Fonksiyon bitince Done çağır
16 c.Add(1) // c'ye 1 ekle
17 }()
18 }
19
20 wg.Wait() // Tüm goroutine'lerin bitmesini bekle
21 println(c.Load()) // c'nin değerini yazdır
22}
Bu, daha önce kullanılan bir örnektir. atomic.Int64 tipi kullanılarak c değişkenini atomik olarak artıran bir koddur. Add metodu ve Load metodu ile değişkeni atomik olarak artırabilir ve okuyabiliriz. Ayrıca Store metodu ile değer saklayabilir, Swap metodu ile değerleri değiştirebilir ve CompareAndSwap metodu ile değerleri karşılaştırıp uygunsa değiştirebiliriz.
cond
sync.Cond
cond paketi, koşul değişkenleri sağlayan bir pakettir. cond paketi sync.Cond ile oluşturulabilir ve Wait, Signal, Broadcast metodlarını sunar.
1package main
2
3import (
4 "sync"
5)
6
7func main() {
8 c := sync.NewCond(&sync.Mutex{}) // Yeni bir Cond oluştur
9 ready := false // Hazırlık durumu değişkeni
10
11 go func() {
12 c.L.Lock() // Kilitle
13 ready = true // Hazırlık durumunu true yap
14 c.Signal() // Bir bekleyen goroutine'i uyandır
15 c.L.Unlock() // Kilidi aç
16 }()
17
18 c.L.Lock() // Kilitle
19 for !ready { // ready false olduğu sürece
20 c.Wait() // Bekle
21 }
22 c.L.Unlock() // Kilidi aç
23
24 println("Ready!") // "Hazır!" yazdır
25}
Yukarıdaki kod, sync.Cond kullanarak ready değişkeni true olana kadar bekleyen bir koddur. Bu kodda, sync.Cond kullanılarak ready değişkeni true olana kadar beklenir ve ardından Ready! çıktısı verilir. Bu şekilde sync.Cond kullanarak, birden fazla goroutine'in aynı anda belirli bir koşulun sağlanmasını beklemesini sağlayabilirsiniz.
Bunu kullanarak basit bir queue implemente edebiliriz.
1package queue
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8type Node[T any] struct { // Node yapısı
9 Value T // Değer
10 Next *Node[T] // Sonraki düğüm
11}
12
13type Queue[T any] struct { // Kuyruk yapısı
14 sync.Mutex // Mutex
15 Cond *sync.Cond // Koşul değişkeni
16 Head *Node[T] // Baş düğüm
17 Tail *Node[T] // Kuyruk düğüm
18 Len int // Uzunluk
19}
20
21func New[T any]() *Queue[T] { // Yeni kuyruk oluştur
22 q := &Queue[T]{} // Kuyruk nesnesi oluştur
23 q.Cond = sync.NewCond(&q.Mutex) // Cond'u mutex ile ilişkilendir
24 return q // Kuyruğu döndür
25}
26
27func (q *Queue[T]) Push(value T) { // Kuyruğa değer ekle
28 q.Lock() // Kilitle
29 defer q.Unlock() // Kilidi açmayı ertele
30
31 node := &Node[T]{Value: value} // Yeni düğüm oluştur
32 if q.Len == 0 { // Kuyruk boşsa
33 q.Head = node // Baş düğüm yeni düğüm olur
34 q.Tail = node // Kuyruk düğüm yeni düğüm olur
35 } else { // Kuyruk boş değilse
36 q.Tail.Next = node // Kuyruk düğümün sonraki yeni düğüm olur
37 q.Tail = node // Kuyruk düğüm yeni düğüm olur
38 }
39 q.Len++ // Uzunluğu artır
40 q.Cond.Signal() // Bir bekleyen goroutine'i uyandır
41}
42
43func (q *Queue[T]) Pop() T { // Kuyruktan değer çıkar
44 q.Lock() // Kilitle
45 defer q.Unlock() // Kilidi açmayı ertele
46
47 for q.Len == 0 { // Kuyruk boş olduğu sürece
48 q.Cond.Wait() // Bekle
49 }
50
51 node := q.Head // Baş düğümü al
52 q.Head = q.Head.Next // Baş düğümü sonraki düğüme taşı
53 q.Len-- // Uzunluğu azalt
54 return node.Value // Değeri döndür
55}
Bu şekilde sync.Cond kullanarak, spin-lock ile çok fazla CPU kullanımı yerine verimli bir şekilde bekleyebilir ve koşul sağlandığında tekrar çalışmaya başlayabiliriz.
semaphore
golang.org/x/sync/semaphore.Semaphore
semaphore paketi, semafor sağlayan bir pakettir. semaphore paketi golang.org/x/sync/semaphore.Semaphore ile oluşturulabilir ve Acquire, Release, TryAcquire metodlarını sunar.
1package main
2
3import (
4 "context"
5 "fmt"
6 "golang.org/x/sync/semaphore"
7)
8
9func main() {
10 s := semaphore.NewWeighted(1) // Ağırlıklı semafor oluştur
11
12 if s.TryAcquire(1) { // 1 birim kaynak elde etmeye çalış
13 fmt.Println("Acquired!") // "Elde edildi!" yazdır
14 } else {
15 fmt.Println("Not Acquired!") // "Elde edilmedi!" yazdır
16 }
17
18 s.Release(1) // 1 birim kaynağı serbest bırak
19}
Yukarıdaki kod, semaphore kullanarak bir semafor oluşturur ve semaforu kullanarak Acquire metodu ile semaforu elde edip, Release metodu ile semaforu serbest bırakan bir koddur. Bu kodda, semaphore kullanarak semaforu elde etme ve serbest bırakma yöntemi gösterilmiştir.
Sonuç
Temel içerik bu kadar yeterli olacaktır. Bu makalenin içeriğini temel alarak, goroutine'leri kullanarak eşzamanlılığı yönetme yöntemlerini anlamanızı ve bunları pratik olarak kullanabilmenizi dilerim. Bu makalenin sizlere yardımcı olmasını umuyorum. Teşekkür ederim.