Go programlama dili, eşzamanlı (concurrent) işlemleri yönetmek için birçok araca sahiptir. Bu yazıda bu araçlardan bazılarını ve işe yarayan bazı ipuçlarını paylaşacağız.

Goroutine Ne Demek?

Goroutine, Go dilinde desteklenen yeni bir eşzamanlılık modeli. Genellikle programlar, birden fazla görevi eşzamanlı olarak gerçekleştirmek için işletim sisteminden OS thread’leri alır ve çekirdek sayısı kadar görevi paralel olarak yürütür. Daha ince taneli eşzamanlılık için kullanıcı seviyesinde green thread’ler oluşturulur ve böylece tek bir OS thread içinde birkaç green thread çalışabilir. Ancak goroutine’ler bu green thread’leri daha küçük ve verimli hale getirdi. Bu goroutine’ler thread’lerden daha az bellek kullanır ve thread’lerden daha hızlı oluşturulup değiştirilebilir.

Bir goroutine kullanmak için sadece go anahtar kelimesini kullanmak yeterli. Bu, program yazma sürecinde senkron kodu asenkron kod olarak sezgisel bir şekilde çalıştırmayı sağlıyor.

Go
package main

import (
    "fmt"
    "time"
)

func main() {
    done := make(chan bool)
    
    go func() {
        defer func() { done <- true }()
        time.Sleep(2 * time.Second)
        fmt.Println("İşlem tamamlandı!")
    }()
    
    fmt.Println("Goroutine başlatıldı, bekliyor...")
    <-done
    fmt.Println("Program sonlandı")
}

Bu kod basitçe 2 saniye bekleyip İşlem tamamlandı! yazdıran senkron bir kodun asenkron akışa dönüştürülmüş hali. Örnek basit görünse de, biraz daha karmaşık kodlar senkrondan asenkrona çevrildiğinde, kodun okunabilirliği, görünürlüğü ve anlaşılabilirliği geleneksel async await veya promise gibi yöntemlerden çok daha iyi oluyor.

Ancak çoğu durumda, bu tür senkron kodları asenkron olarak çağırmanın akışını ve fork & join gibi akışları (böl ve fethet mantığına benzer bir akış) anlamadan sadece kullanmaya kalktığımızda kötü goroutine kodları ortaya çıkabiliyor. Bu yazımda, böyle durumlar için hazırlıklı olabileceğimiz birkaç yöntem ve tekniği tanıtacağım.

Eşzamanlılık(Concurrency) Yönetimi

context

İlk teknik olarak context‘in karşımıza çıkması belki de sürpriz olabilir. Ama Go’da context sadece basit iptal işlemlerinden çok daha fazlasını yapıyor – tüm çalışma döngüsünü yönetmede gerçekten güçlü bir araç. Bu konuya yabancı olanlar için hızlıca değineyim.

Go
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    go func() {
        <-ctx.Done()
        fmt.Println("Context iptal edildi!")
    }()

    time.Sleep(2 * time.Second)
    
    cancel()
    
    time.Sleep(500 * time.Millisecond)
}

Yukarıdaki örnekte context ile 2 saniye bekleyip Context iptal edildi! mesajını bastırıyoruz. context paketinin Done() metodu sayesinde iptal durumunu takip edebiliyoruz ve WithCancel, WithTimeout, WithDeadline, WithValue gibi farklı iptal yöntemleri kullanabiliyoruz.

Gerçek hayattan bir senaryo düşünelim. Bir dashboard için kullanıcı bilgisi, sipariş geçmişi ve bildirimler verilerini paralel olarak çekmeniz gerekiyor. Bu işlemlerin 3 saniye içinde bitmesi şart, yoksa kullanıcı deneyimi bozuluyor:

Go
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    finished := make(chan bool)
    go func() {
        defer close(finished)
        profile := getProfile(ctx)
        orders := getOrders(ctx)
        notifications := getNotifications(ctx)

        fmt.Printf("Veriler: %s, %s, %s\n", profile, orders, notifications)
    }()

    select {
    case <-ctx.Done():
        fmt.Println("Zaman aşımı! İşlem iptal edildi")
    case <-finished:
        fmt.Println("Tüm veriler başarıyla alındı!")
    }
}

func getProfile(ctx context.Context) string {
    time.Sleep(1 * time.Second)
    return "profile-data"
}

func getOrders(ctx context.Context) string {
    time.Sleep(1 * time.Second)
    return "orders-data"
}

func getNotifications(ctx context.Context) string {
    time.Sleep(1 * time.Second)
    return "notifications-data"
}

Bu kod yapısında, eğer veriler belirlenen sürede gelmezse Zaman aşımı! İşlem iptal edildi uyarısı veriyor, her şey yolunda giderse Tüm veriler başarıyla alındı! onayını alıyoruz. context sayesinde birden çok goroutine ile çalışırken bile iptal ve zaman aşımı kontrollerini çok temiz bir şekilde halledebiliyoruz.

Context’in tüm özelliklerini godoc context dokümantasyonundan öğrenebilirsiniz. Bu temelleri kavrayıp günlük kodlamada rahatça kullanmanızı tavsiye ederim.

channel

unbuffered channel

channel goroutine’ler arasında iletişim kurmak için kullandığımız bir araç. make(chan T) ile bir channel oluşturabiliyoruz. Burada T, channel‘ın taşıyacağı verinin tipi. <- operatörü ile veri gönderip alabiliyor ve close ile channel‘ı kapatabiliyoruz.

Go
package main

import "fmt"

func main() {
    messages := make(chan string)
    
    go func() {
        messages <- "Merhaba"
        messages <- "Dünya"
        close(messages)
    }()

    for msg := range messages {
        fmt.Println(msg)
    }
}

Yukarıdaki kod bir channel kullanarak “Merhaba” ve “Dünya” yazdırıyor. Bu kod channel üzerinden değer gönderme ve alma işlemlerini basitçe gösteriyor. Ancak channel‘ın daha fazla özelliği var. Önce buffered channel ve unbuffered channel arasındaki farkı inceleyelim. Yukarıda yazdığımız örnek unbuffered channel, yani channel’a veri gönderme işlemi ile veri alma işlemi aynı anda gerçekleşmek zorunda. Bu işlemler eş zamanlı olmazsa deadlock durumu ortaya çıkabilir.

buffered channel

Yukarıdaki kod sadece yazdırma yapmak yerine ağır işlemler yapan iki süreç olsaydı ne olurdu? İkinci süreç okuma yaptıktan sonra işlem sırasında uzun süre takılırsa, birinci süreç de o süre boyunca duracaktır. Bu tür durumları önlemek için buffered channel kullanabiliriz.

Go
package main

import (
    "fmt"
    "time"
)

func main() {
    tasks := make(chan string, 3)
    
    go func() {
        tasks <- "İlk görev"
        tasks <- "İkinci görev"
        tasks <- "Üçüncü görev"
        close(tasks)
    }()

    for task := range tasks {
        fmt.Println("İşleniyor:", task)
        time.Sleep(1 * time.Second)
    }
}

Yukarıdaki kod buffered channel kullanarak görevleri işliyor. Bu kodda buffered channel kullanarak channel‘a veri gönderme işlemi ile veri alma işleminin aynı anda gerçekleşmesini zorunlu olmaktan çıkardık. Channel’da bu şekilde buffer bulunması, uzunluğa karşılık gelen bir hareket alanı yaratıyor ve downstream görevlerinin neden olduğu gecikmeleri önleyebiliyor.

select

Birden fazla channel ile çalışırken, select statement kullanarak kolayca bir fan-in yapısı kurabiliyoruz.

Go
package main

import (
    "fmt"
    "time"
)

func main() {
    emailCh := make(chan string, 5)
    smsCh := make(chan string, 5)
    pushCh := make(chan string, 5)

    go func() {
        for {
            emailCh <- "Email bildirimi"
            time.Sleep(500 * time.Millisecond)
        }
    }()
    
    go func() {
        for {
            smsCh <- "SMS bildirimi"
            time.Sleep(1500 * time.Millisecond)
        }
    }()
    
    go func() {
        for {
            pushCh <- "Push bildirimi"
            time.Sleep(2500 * time.Millisecond)
        }
    }()

    for i := 0; i < 5; i++ {
        select {
        case notification := <-emailCh:
            fmt.Println("Alındı:", notification)
        case notification := <-smsCh:
            fmt.Println("Alındı:", notification)
        case notification := <-pushCh:
            fmt.Println("Alındı:", notification)
        }
    }
}

Yukarıdaki kod farklı aralıklarla bildirim gönderen üç channel oluşturuyor ve select kullanarak bu channel’lardan gelen değerleri alıp yazdırıyor. select‘i bu şekilde kullanarak birden fazla channel’dan eş zamanlı olarak veri alabilir ve channel’lardan gelen değerleri aldığımız anda işleyebiliriz.

for range

channel‘dan veri almak için for range kullanabiliyoruz. for range bir channel ile kullanıldığında, o channel’a veri eklendiği her seferde çalışır ve channel kapatıldığında döngü sona erer.

Go
package main

import "fmt"

func main() {
    numbers := make(chan int)
    
    go func() {
        numbers <- 10
        numbers <- 20
        numbers <- 30
        close(numbers)
    }()

    for num := range numbers {
        fmt.Println("Sayı:", num)
    }
}

Yukarıdaki kod bir channel kullanarak sayıları yazdırıyor. Bu kod for range kullanarak channel’a veri eklendiği her seferde veriyi alıp yazdırıyor. Channel kapatıldığında döngü sona eriyor.
Yukarıda birkaç kez yazdığım gibi, bu syntax basit bir senkronizasyon yöntemi olarak da kullanılabilir.

Go
package main

import (
    "fmt"
    "time"
)

func main() {
    done := make(chan struct{})
    
    go func() {
        defer close(done)
        time.Sleep(1500 * time.Millisecond)
        fmt.Println("İşlem bitti!")
    }()

    fmt.Println("Goroutine çalışıyor, bekleniyor...")
    for range done {}
    fmt.Println("Program tamamlandı")
}

Yukarıdaki kod 1.5 saniye bekleyip İşlem bitti! yazdırıyor. Bu kod channel kullanarak senkron kodu asenkron koda dönüştürmüş. channel‘ı bu şekilde kullanarak senkron kodları kolayca asenkron koda çevirebilir ve join noktasını belirleyebilirsiniz.

diğer notlar

  1. nil olan bir channel’a veri göndermek veya almak sonsuz döngüye yol açar ve deadlock oluşturur.
  2. Kapalı bir channel’a veri göndermek panic’e neden olur.
  3. Channel’ı açık şekilde kapatmak zorunda değilsiniz; GC toplama işlemi sırasında kendisi kapatacaktır.

mutex

spinlock

spinlock sürekli olarak kilit almaya çalışan bir senkronizasyon yöntemi. Go’da spinlock kolayca pointer kullanarak implemente edilebilir.

Go
package spinlock

import (
    "runtime"
    "sync/atomic"
)

type SpinLock struct {
    locked uint32
}

func (s *SpinLock) Lock() {
    for !atomic.CompareAndSwapUint32(&s.locked, 0, 1) {
        runtime.Gosched()
    }
}

func (s *SpinLock) Unlock() {
    atomic.StoreUint32(&s.locked, 0)
}

func NewSpinLock() *SpinLock {
    return &SpinLock{}
}

Yukarıdaki kod spinlock paketini implement ediyor. Bu kod sync/atomic paketini kullanarak SpinLock yapısını oluşturuyor. Lock metodu atomic.CompareAndSwapUint32 kullanarak kilidi almaya çalışıyor ve Unlock metodu atomic.StoreUint32 ile kilidi serbest bırakıyor. Bu yöntem duraklamadan sürekli kilit almaya çalışır; bu nedenle kilit alınana kadar CPU’yu sürekli kullanır ve sonsuz döngüye yol açabilir. Dolayısıyla spinlock basit senkronizasyon için veya sadece kısa süreli kullanım durumlarında en iyisidir.

sync.Mutex

mutex goroutineleri senkronize etmek için kullandığımız bir araç. sync paketinin sunduğu mutex Lock, Unlock, RLock ve RUnlock gibi metodlar sunuyor. sync.Mutex ile bir mutex oluşturabilir, ayrıca sync.RWMutex ile okuma/yazma kilidi de kullanabilirsiniz.

Go
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var mu sync.Mutex
    var total int

    go func() {
        mu.Lock()
        defer mu.Unlock()
        time.Sleep(100 * time.Millisecond)
        total += 50
    }()

    mu.Lock()
    total += 25
    mu.Unlock()

    time.Sleep(200 * time.Millisecond)
    fmt.Println("Toplam:", total)
}

Yukarıdaki kodda iki goroutine aynı total değişkenine neredeyse eş zamanlı erişiyor. total değişkenine erişen kodu mutex kullanarak kritik bölüm haline getirerek, total değişkenine eş zamanlı erişimi engelleyebiliyoruz. Böylece bu kod kaç kez çalıştırılırsa çalıştırılsın her zaman 75 yazdıracak.

sync.RWMutex

sync.RWMutex okuma ve yazma kilitlerinin ayrı ayrı kullanılmasına izin veren bir mutex. Okuma kilitleri RLock ve RUnlock metodları kullanılarak alınıp serbest bırakılabilir.

Go
package cmap

import (
    "sync"
)

type SafeMap[K comparable, V any] struct {
    sync.RWMutex
    items map[K]V
}

func (m *SafeMap[K, V]) Read(key K) (V, bool) {
    m.RLock()
    defer m.RUnlock()

    value, exists := m.items[key]
    return value, exists
}

func (m *SafeMap[K, V]) Write(key K, value V) {
    m.Lock()
    defer m.Unlock()

    if m.items == nil {
        m.items = make(map[K]V)
    }
    m.items[key] = value
}

func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
    return &SafeMap[K, V]{
        items: make(map[K]V),
    }
}

Yukarıdaki kod sync.RWMutex kullanarak SafeMap implement ediyor. Bu kod Read metodunda okuma kilidi, Write metodunda yazma kilidi kullanarak items map’ine güvenli erişim ve değişiklik yapıyor. Okuma kilidinin gerekli olmasının sebebi, çok sayıda basit okuma işlemi olduğunda yazma kilidi almaya gerek kalmadan birden fazla goroutine’in eş zamanlı okuma işlemi yapabilmesine izin vermek. Bu durum değişiklik olmadığı için yazma kilidine gerek olmadığında sadece okuma kilidi kullanarak performansı artırıyor.

fakelock

fakelock sync.Locker implement eden basit bir numara. Bu struct sync.Mutex ile aynı metodları sunuyor ama aslında hiçbir işlem yapmıyor.

Go
package fakelock

type FakeLock struct{}

func (f *FakeLock) Lock() {}

func (f *FakeLock) Unlock() {}

func NewFakeLock() *FakeLock {
    return &FakeLock{}
}

Yukarıdaki kod fakelock paketini implement ediyor. Bu paket sync.Locker implement ederek Lock ve Unlock metodları sunuyor ama gerçekte hiçbir şey yapmıyor. Bu kodun neden gerekli olduğunu fırsat bulduğumda anlatacağım.

waitgroup

sync.WaitGroup

sync.WaitGroup tüm goroutine görevleri tamamlanana kadar bekleyen bir araç. Add, Done ve Wait metodları sunuyor. Add metodu goroutine sayısını eklemek için, Done metodu bir goroutine’in görevinin tamamlandığını belirtmek için, Wait metodu ise tüm goroutinelerin görevleri bitene kadar beklemek için kullanılıyor.

Go
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
    "time"
)

func main() {
    wg := sync.WaitGroup{}
    counter := atomic.Int64{}

    for i := 0; i < 50; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            time.Sleep(100 * time.Millisecond)
            counter.Add(1)
            fmt.Printf("Görev %d tamamlandı\n", id)
        }(i)
    }

    wg.Wait()
    fmt.Printf("Toplam tamamlanan görev: %d\n", counter.Load())
}

Yukarıdaki kod sync.WaitGroup kullanarak 50 goroutine’in eş zamanlı olarak counter değişkeninin değerini artırmasını sağlıyor. Bu kodda sync.WaitGroup tüm goroutinelerin tamamlanmasını bekleyip sonra counter değişkeninin birikmiş değerini yazdırıyor. Basit fork & join işlemleri için channel kullanmak yeterli olsa da, çok sayıda fork & join görevini yönetirken sync.WaitGroup kullanmak güzel bir alternatif sunuyor.

slice ile kullanımı

Slice’lar ile birlikte kullanıldığında waitgroup kilit gerektirmeden eş zamanlı çalışma görevlerini yönetmek için mükemmel bir araç olarak işlev görebilir.

Go
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    results := [8]int{}

    for i := 0; i < 8; i++ {
        wg.Add(1)
        go func(index int) {
            defer wg.Done()
            
            time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
            results[index] = rand.Intn(1000)
            fmt.Printf("İndeks %d işlendi\n", index)
        }(i)
    }

    wg.Wait()
    fmt.Println("Tüm işlemler tamamlandı!")

    for i, value := range results {
        fmt.Printf("results[%d] = %d\n", i, value)
    }
}

Yukarıdaki kod sadece waitgroup kullanarak her goroutine’de rastgele tamsayılar üretiyor ve bunları atanan indekste saklıyor. Bu kodda waitgroup tüm goroutineler bitene kadar beklemek için kullanılıyor, sonra Tüm işlemler tamamlandı! yazdırılıyor. waitgroup‘u bu şekilde kullanarak birden fazla goroutine’in eş zamanlı görev yapmasını, tüm goroutineler bitene kadar kilit olmadan veri saklamasını ve görevler tamamlandıktan sonra toplu işlem yapılmasını sağlayabiliyoruz.

golang.org/x/sync/errgroup.ErrGroup

errgroup sync.WaitGroup‘u genişleten bir paket. sync.WaitGroup‘tan farklı olarak, goroutinelerden herhangi birinde hata oluşursa errgroup tüm goroutineleri iptal eder ve hatayı döndürür.

Go
package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/errgroup"
    "time"
)

func main() {
    g, ctx := errgroup.WithContext(context.Background())
    _ = ctx

    for i := 0; i < 8; i++ {
        taskID := i
        g.Go(func() error {
            time.Sleep(100 * time.Millisecond)
            
            if taskID == 3 {
                return fmt.Errorf("görev %d'de hata oluştu", taskID)
            }
            
            fmt.Printf("Görev %d başarılı\n", taskID)
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        fmt.Printf("Hata yakalandı: %v\n", err)
    } else {
        fmt.Println("Tüm görevler başarıyla tamamlandı")
    }
}

Yukarıdaki kod errgroup kullanarak 8 goroutine oluşturuyor ve 3. goroutine’de hata oluşturuyor. Hata kasıtlı olarak üçüncü goroutine’de oluşturuluyor böylece hata durumunu gösterebiliyoruz. Ama gerçek kullanımda errgroup goroutineler oluşturmak ve her goroutine’de hata oluştuğunda çeşitli işlemleri yönetmek için kullanılabilir.

Once

Sadece bir kez çalıştırılması gereken kodları yürütmek için kullanılan bir araç. İlgili kodu çalıştırmak için aşağıdaki constructor’lar kullanılabilir.

func OnceFunc(f func()) func()
func OnceValue[T any](f func() T) func() T
func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)

OnceFunc

OnceFunc belirli bir fonksiyonun tüm çalışma boyunca sadece bir kez çalıştırılmasını sağlıyor.

Go
package main

import (
    "fmt"
    "sync"
)

func main() {
    initFunc := sync.OnceFunc(func() {
        fmt.Println("Uygulama başlatılıyor...")
        fmt.Println("Ayarlar yüklendi!")
    })

    fmt.Println("İlk çağrı:")
    initFunc()
    
    fmt.Println("İkinci çağrı:")
    initFunc()
    
    fmt.Println("Üçüncü çağrı:")
    initFunc()
    
    fmt.Println("Program devam ediyor...")
}

Yukarıdaki kod sync.OnceFunc kullanarak uygulama başlatma mesajlarını yazdırıyor. Bu kodda sync.OnceFunc ile initFunc fonksiyonu oluşturuluyor ve initFunc fonksiyonu birden fazla kez çağrılsa bile başlatma mesajları sadece bir kez yazdırılıyor.

OnceValue

OnceValue fonksiyonun tüm çalışma boyunca sadece bir kez çalıştırılmasını sağlamanın yanında, fonksiyonun dönüş değerini de saklıyor ve tekrar çağrıldığında saklanan değeri döndürüyor.

Go
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func main() {
    randomID := 0
    
    generateID := sync.OnceValue(func() int {
        fmt.Println("ID üretiliyor...")
        time.Sleep(100 * time.Millisecond)
        randomID = rand.Intn(10000) + 1000
        fmt.Printf("Üretilen ID: %d\n", randomID)
        return randomID
    })

    fmt.Println("ID'yi birinci kez alıyorum:")
    id1 := generateID()
    fmt.Printf("Alınan ID: %d\n\n", id1)
    
    fmt.Println("ID'yi ikinci kez alıyorum:")
    id2 := generateID()
    fmt.Printf("Alınan ID: %d\n\n", id2)
    
    fmt.Println("ID'yi üçüncü kez alıyorum:")
    id3 := generateID()
    fmt.Printf("Alınan ID: %d\n", id3)
}

Yukarıdaki kod sync.OnceValue kullanarak rastgele bir ID üretiyor. Bu kodda sync.OnceValue ile generateID fonksiyonu oluşturuluyor ve generateID fonksiyonu birden fazla kez çağrılsa bile ID üretme işlemi sadece bir kez yapılıyor ve aynı ID değeri döndürülüyor.

OnceValues

OnceValues OnceValue ile aynı şekilde çalışıyor ama birden fazla değer döndürebiliyor.

Go
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    connectionCount := 0
    
    createConnection := sync.OnceValues(func() (string, int) {
        fmt.Println("Veritabanı bağlantısı kuruluyor...")
        time.Sleep(200 * time.Millisecond)
        connectionCount++
        
        host := "localhost:5432"
        port := 5432
        
        fmt.Printf("Bağlantı kuruldu: %s:%d\n", host, port)
        return host, port
    })

    fmt.Println("İlk bağlantı talebi:")
    host1, port1 := createConnection()
    fmt.Printf("Dönen değerler: %s, %d\n\n", host1, port1)
    
    fmt.Println("İkinci bağlantı talebi:")
    host2, port2 := createConnection()
    fmt.Printf("Dönen değerler: %s, %d\n\n", host2, port2)
    
    fmt.Println("Üçüncü bağlantı talebi:")
    host3, port3 := createConnection()
    fmt.Printf("Dönen değerler: %s, %d\n\n", host3, port3)
    
    fmt.Printf("Toplam bağlantı sayısı: %d\n", connectionCount)
}

Yukarıdaki kod sync.OnceValues kullanarak veritabanı bağlantı bilgilerini oluşturuyor. Bu kodda sync.OnceValues ile createConnection fonksiyonu oluşturuluyor ve createConnection fonksiyonu birden fazla kez çağrılsa bile bağlantı kurma işlemi sadece bir kez yapılıyor ve aynı host ile port değerleri döndürülüyor.

atomic

atomic paketi, Go dilinde atomik işlemler gerçekleştirmemizi sağlayan güçlü bir araçtır. Bu paket Add, CompareAndSwap, Load, Store ve Swap gibi metodlar sunuyor, ancak son zamanlarda Int64, Uint64 ve Pointer gibi tiplerin kullanımı öneriliyor.

Go
package main

import (
    "sync"
    "sync/atomic"
)

func main() {
    wg := sync.WaitGroup{}
    counter := atomic.Int64{}

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Add(1)
        }()
    }

    wg.Wait()
    println("Toplam sayaç değeri:", counter.Load())
}

Bu örnek, daha önce kullandığımız bir kod parçasının geliştirilmiş halidir. Burada atomic.Int64 tipini kullanarak counter değişkenini atomik olarak artırıyoruz. Add metoduyla değişkeni atomik şekilde artırabilir, Load metoduyla da değişkeni güvenli bir şekilde okuyabiliriz.

Bunlara ek olarak:

  • Store metodu: Bir değeri atomik olarak saklamak için
  • Swap metodu: Değeri atomik olarak değiştirmek için
  • CompareAndSwap metodu: Bir değeri karşılaştırıp eşleşirse değiştirmek için

Bu metodları kullanabiliriz. Bu yaklaşım, race condition’ları önlemek ve thread-safe kod yazmak için oldukça etkili bir yöntemdir.

cond sync.Cond

sync.Cond paketi, condition variable’lar sağlayan bir pakettir. sync.Cond kullanarak oluşturulabilir ve Wait, Signal, ve Broadcast metodlarını sunar.

Go
package main

import (
    "sync"
    "time"
)

func main() {
    c := sync.NewCond(&sync.Mutex{})
    ready := false

    go func() {
        time.Sleep(2 * time.Second)
        c.L.Lock()
        ready = true
        c.Signal()
        c.L.Unlock()
    }()

    c.L.Lock()
    for !ready {
        c.Wait()
    }
    c.L.Unlock()

    println("Hazır!")
}

Yukarıdaki kod sync.Cond kullanarak ready değişkeni true olana kadar bekliyor. Bu kodda, sync.Cond ile ready değişkeninin true olmasını bekleyip, ardından “Hazır!” yazdırıyoruz. sync.Cond‘u bu şekilde kullanmak, birden fazla goroutine’in aynı anda belirli bir koşulun sağlanmasını beklemesine olanak tanır.

Bu yöntemi kullanarak basit bir queue implementasyonu yapabiliriz:

Go
package queue

import (
    "sync"
)

type Node[T any] struct {
    Value T
    Next  *Node[T]
}

type Queue[T any] struct {
    sync.Mutex
    Cond *sync.Cond
    Head *Node[T]
    Tail *Node[T]
    Len  int
}

func New[T any]() *Queue[T] {
    q := &Queue[T]{}
    q.Cond = sync.NewCond(&q.Mutex)
    return q
}

func (q *Queue[T]) Push(value T) {
    q.Lock()
    defer q.Unlock()

    node := &Node[T]{Value: value}
    if q.Len == 0 {
        q.Head = node
        q.Tail = node
    } else {
        q.Tail.Next = node
        q.Tail = node
    }
    q.Len++
    q.Cond.Signal()
}

func (q *Queue[T]) Pop() T {
    q.Lock()
    defer q.Unlock()

    for q.Len == 0 {
        q.Cond.Wait()
    }

    node := q.Head
    q.Head = q.Head.Next
    q.Len--
    return node.Value
}

sync.Cond‘u bu şekilde kullanarak, CPU kaynaklarını çok fazla kullanan spin-lock yerine, bir koşul sağlandığında etkili bir şekilde bekleyip işleme devam edebiliriz. Bu yaklaşım özellikle producer-consumer pattern’lerinde oldukça kullanışlıdır.

semaphore

semaphore paketi, semafor işlemleri sağlayan bir pakettir. golang.org/x/sync/semaphore.Semaphore kullanarak oluşturulabilir ve Acquire, Release, ve TryAcquire metodlarını sunar.

Go
package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/semaphore"
    "time"
)

func main() {
    s := semaphore.NewWeighted(2)
    ctx := context.Background()

    if s.TryAcquire(1) {
        fmt.Println("İlk kaynak alındı!")
        defer s.Release(1)
    }

    err := s.Acquire(ctx, 1)
    if err == nil {
        fmt.Println("İkinci kaynak alındı!")
        time.Sleep(1 * time.Second)
        s.Release(1)
    }

    if s.TryAcquire(1) {
        fmt.Println("Üçüncü deneme başarılı!")
        s.Release(1)
    } else {
        fmt.Println("Kaynak meşgul, alınamadı!")
    }
}

Yukarıdaki kod semaphore paketini kullanarak bir semafor oluşturuyor, TryAcquire metoduyla semaforu almaya çalışıyor ve Release metoduyla semaforu serbest bırakıyor. Bu kodda, semaphore kullanarak nasıl kaynak alınıp serbest bırakılacağını gösterdik.

Semaforlar özellikle eş zamanlı erişimi sınırlamak istediğimiz durumlarda çok kullanışlıdır. Örneğin, aynı anda sadece belirli sayıda goroutine’in bir kaynağa erişmesini istiyorsak semaforu bu amaçla kullanabiliriz.

Kaynaklar:

https://gosuda.org/blog/posts/go-concurrency-starter-pack-z221a3399

https://go.dev/tour/concurrency/11

Kategoriler:

Golang, Yazılım,