GoSuda

Go Eşzamanlılık Başlangıç Paketi

By snowmerak
views ...

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.

  1. nil channel'a veri gönderilir veya nil channel'dan veri alınırsa, sonsuz döngüye girilerek deadlock oluşabilir.
  2. Kanal kapatıldıktan sonra veri gönderilirse, panic oluşur.
  3. 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.