Go'da Eşzamanlılık: Goroutine'ler ve Channel'lar - Eksiksiz Rehber

Goroutine'ler ve channel'larla Go eşzamanlılığında ustalaşın. Gelişmiş desenler, senkronizasyon, select ifadeleri ve detaylı kod örnekleriyle en iyi uygulamalar.

Go Eşzamanlılık - Goroutine'ler ve channel'lar iş başında

Eşzamanlılık, Go'nun en büyük güçlerinden birini oluşturur. Çoklu iş parçacığının karmaşık kaldığı diğer dillerin aksine, Go, eşzamanlı uygulama geliştirmeyi önemli ölçüde basitleştiren goroutine'ler ve channel'lara dayalı zarif bir model sunar.

Go Felsefesi

"Belleği paylaşarak iletişim kurmayın; iletişim kurarak belleği paylaşın." Bu temel ilke, Go'daki tüm eşzamanlılık tasarımına yön verir.

Goroutine'leri Anlamak

Goroutine'ler, Go runtime'ı tarafından yönetilen hafif iş parçacıklarıdır. Yaklaşık 2 KB stack alanı tüketir (işletim sistemi iş parçacıkları için birkaç MB'a karşı) ve sistem yükü olmadan binlerce eşzamanlı görevin çalıştırılmasına olanak tanır.

Bir goroutine başlatmak, yalnızca bir fonksiyon çağrısının önüne go anahtar kelimesini koymayı gerektirir. Runtime, mevcut iş parçacıkları arasında zamanlama ve dağıtımı üstlenir.

goroutines_basic.gogo
package main

import (
    "fmt"
    "time"
)

// fetchData simulates a network request
func fetchData(id int) {
    // Simulates network delay
    time.Sleep(100 * time.Millisecond)
    fmt.Printf("Data %d fetched\n", id)
}

func main() {
    // Sequential execution - 500ms total
    start := time.Now()
    for i := 1; i <= 5; i++ {
        fetchData(i)
    }
    fmt.Printf("Sequential: %v\n", time.Since(start))

    // Concurrent execution - ~100ms total
    start = time.Now()
    for i := 1; i <= 5; i++ {
        go fetchData(i) // Execute as goroutine
    }
    time.Sleep(150 * time.Millisecond) // Wait for completion
    fmt.Printf("Concurrent: %v\n", time.Since(start))
}

Eşzamanlı yürütme, toplam süreyi 500ms'den yaklaşık 100ms'ye indirir. Ancak goroutine'leri senkronize etmek için time.Sleep kullanmak iyi bir uygulama değildir. Channel'lar zarif bir çözüm sunar.

Channel'lar: Goroutine'ler Arası İletişim

Bir channel, goroutine'ler arasında değer göndermek ve almak için tipli bir kanaldır. Channel'lar senkronizasyonu garanti eder: gönderen bir goroutine, başka bir tanesinin almasına kadar bekler ve tersi de geçerlidir.

Channel oluşturma make fonksiyonunu kullanır. <- operatörü, channel'a göre konumuna bağlı olarak veri gönderir ve alır.

channels_basic.gogo
package main

import "fmt"

// worker performs computation and returns result via channel
func worker(id int, jobs <-chan int, results chan<- int) {
    // Receives jobs until channel closes
    for job := range jobs {
        result := job * 2 // Processing
        results <- result // Send result
    }
}

func main() {
    // Create channels
    jobs := make(chan int, 10)    // Buffered channel
    results := make(chan int, 10)

    // Start 3 workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Send 5 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs) // Signal end of jobs

    // Collect results
    for r := 1; r <= 5; r++ {
        result := <-results
        fmt.Printf("Result: %d\n", result)
    }
}

Yönlü channel'lar (alma için <-chan, gönderme için chan<-), olası işlemleri sınırlayarak kod güvenliğini artırır.

Tamponlu vs Tamponsuz Channel'lar

Bu channel türleri arasındaki ayrım, goroutine'ler arasındaki senkronizasyon davranışını doğrudan etkiler.

Tamponsuz channel'lar, bir alıcı hazır olana kadar göndericiyi engeller. Tamponlu channel'lar, N tampon kapasitesini temsil ettiğinde, engellemeden N değere kadar göndermeye olanak tanır.

buffered_channels.gogo
package main

import "fmt"

func main() {
    // Unbuffered channel - strict synchronization
    unbuffered := make(chan string)

    go func() {
        unbuffered <- "message" // Blocks until received
    }()

    msg := <-unbuffered // Unblocks the send
    fmt.Println(msg)

    // Buffered channel - capacity of 2
    buffered := make(chan int, 2)

    // These sends don't block
    buffered <- 1
    buffered <- 2
    // buffered <- 3 // Would block because buffer is full

    fmt.Println(<-buffered) // 1
    fmt.Println(<-buffered) // 2

    // Check capacity
    fmt.Printf("Length: %d, Capacity: %d\n",
        len(buffered), cap(buffered))
}

Tamponlu channel'lar üreticileri ve tüketicileri ayırırken, tamponsuz olanlar noktadan noktaya senkronizasyon garanti eder.

Deadlock'lara Dikkat

Bir deadlock, tüm goroutine'ler bekleyerek engellendiğinde oluşur. Go runtime'ı bunu algılar ve programı açık bir hata mesajıyla sonlandırır.

Select: Channel Çoğullama

select ifadesi, birden fazla channel üzerinde eşzamanlı işlemleri bekler. Eşzamanlı iletişim için bir switch ifadesine benzer.

Bu yapı, zaman aşımlarını, iptalleri ve tek bir channel'da süresiz olarak engellemeden birden fazla iletişimi yönetmek için gereklidir.

select_example.gogo
package main

import (
    "fmt"
    "time"
)

// fetchAPI simulates an API call with variable delay
func fetchAPI(name string, delay time.Duration, ch chan<- string) {
    time.Sleep(delay)
    ch <- fmt.Sprintf("%s: data received", name)
}

func main() {
    api1 := make(chan string)
    api2 := make(chan string)

    // Launch two API calls in parallel
    go fetchAPI("API-1", 100*time.Millisecond, api1)
    go fetchAPI("API-2", 200*time.Millisecond, api2)

    // Global timeout of 150ms
    timeout := time.After(150 * time.Millisecond)

    // Collect results with timeout
    for i := 0; i < 2; i++ {
        select {
        case result := <-api1:
            fmt.Println(result)
        case result := <-api2:
            fmt.Println(result)
        case <-timeout:
            fmt.Println("Timeout - operation cancelled")
            return
        }
    }
}

select, ilk hazır channel'ı seçer. Birden fazlası hazırsa, açlığı önlemek için seçim yalancı rastgeledir.

Go mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Worker Pool Deseni

Worker pool deseni, görevleri birden fazla worker arasında dağıtır, eşzamanlılığı sınırlar ve kaynak kullanımını optimize eder. Bu desen, büyük miktarda veriyi işlemek için vazgeçilmezdir.

Uygulama, worker'lar arasında paylaşılan bir görev channel'ına ve toplama için bir sonuç channel'ına dayanır.

worker_pool.gogo
package main

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

// Task represents a unit of work
type Task struct {
    ID   int
    Data string
}

// Result contains the processing result
type Result struct {
    TaskID int
    Output string
}

// worker processes received tasks
func worker(id int, tasks <-chan Task, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()

    for task := range tasks {
        // Simulate processing
        time.Sleep(50 * time.Millisecond)

        results <- Result{
            TaskID: task.ID,
            Output: fmt.Sprintf("Worker %d processed: %s", id, task.Data),
        }
    }
}

func main() {
    const numWorkers = 3
    const numTasks = 10

    tasks := make(chan Task, numTasks)
    results := make(chan Result, numTasks)

    var wg sync.WaitGroup

    // Start workers
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, tasks, results, &wg)
    }

    // Send tasks
    for i := 1; i <= numTasks; i++ {
        tasks <- Task{ID: i, Data: fmt.Sprintf("task-%d", i)}
    }
    close(tasks)

    // Close results channel after workers finish
    go func() {
        wg.Wait()
        close(results)
    }()

    // Collect results
    for result := range results {
        fmt.Printf("Task %d: %s\n", result.TaskID, result.Output)
    }
}

sync.WaitGroup, sonuç channel'ını kapatmadan önce tüm worker'ların bitmesini beklemeyi koordine eder.

Fan-Out/Fan-In Deseni

Bu desen işi birden fazla goroutine arasında dağıtır (fan-out), ardından sonuçları toplar (fan-in). Sonuçların toplanmasını basitleştirirken paralelliği maksimize eder.

fan_out_fan_in.gogo
package main

import (
    "fmt"
    "sync"
)

// generate produces numbers on a channel
func generate(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

// square computes the square of received numbers
func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

// merge combines multiple channels into one (fan-in)
func merge(channels ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup

    // Output function for each channel
    output := func(c <-chan int) {
        defer wg.Done()
        for n := range c {
            out <- n
        }
    }

    // Launch a goroutine per channel
    wg.Add(len(channels))
    for _, c := range channels {
        go output(c)
    }

    // Close after all goroutines finish
    go func() {
        wg.Wait()
        close(out)
    }()

    return out
}

func main() {
    // Generate data
    numbers := generate(1, 2, 3, 4, 5, 6, 7, 8)

    // Fan-out: distribute to 3 workers
    sq1 := square(numbers)
    sq2 := square(numbers)
    sq3 := square(numbers)

    // Fan-in: aggregate results
    for result := range merge(sq1, sq2, sq3) {
        fmt.Println(result)
    }
}

Bu desen, dağıtılabilir CPU bağımlı işlemler ve veri işleme pipeline'ları için mükemmeldir.

İptal ve Deadline için Context

context paketi, goroutine'ler arasında iptal, deadline ve değer yönetimini standartlaştırır. Uzun süreli herhangi bir goroutine, ilk parametre olarak bir context kabul etmelidir.

context_example.gogo
package main

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

// fetchWithContext simulates a cancellable request
func fetchWithContext(ctx context.Context, url string) (string, error) {
    // Simulates a long operation
    select {
    case <-time.After(2 * time.Second):
        return fmt.Sprintf("Data from %s", url), nil
    case <-ctx.Done():
        return "", ctx.Err() // context.Canceled or context.DeadlineExceeded
    }
}

func main() {
    // Context with 500ms timeout
    ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
    defer cancel() // Release resources

    result, err := fetchWithContext(ctx, "https://api.example.com")
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    fmt.Println(result)

    // Context with manual cancellation
    ctx2, cancel2 := context.WithCancel(context.Background())

    go func() {
        time.Sleep(100 * time.Millisecond)
        cancel2() // Explicit cancellation
    }()

    result, err = fetchWithContext(ctx2, "https://api2.example.com")
    if err != nil {
        fmt.Printf("Request cancelled: %v\n", err)
    }
}
En İyi Uygulama

Kaynak sızıntılarından kaçınmak için bir context oluşturduktan hemen sonra her zaman defer cancel() çağırın.

sync.Mutex ile Senkronizasyon

İletişim için channel'lar tercih edilse de, paylaşılan veri yapılarına eşzamanlı erişimi korumak için sync paketi gereklidir.

mutex_example.gogo
package main

import (
    "fmt"
    "sync"
)

// SafeCounter is a thread-safe counter
type SafeCounter struct {
    mu    sync.Mutex
    value map[string]int
}

// Increment increments the value for a given key
func (c *SafeCounter) Increment(key string) {
    c.mu.Lock()         // Exclusive lock
    defer c.mu.Unlock() // Guaranteed unlock
    c.value[key]++
}

// Value returns the current value
func (c *SafeCounter) Value(key string) int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value[key]
}

func main() {
    counter := SafeCounter{value: make(map[string]int)}

    var wg sync.WaitGroup

    // 1000 concurrent increments
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment("visits")
        }()
    }

    wg.Wait()
    fmt.Printf("Total: %d\n", counter.Value("visits")) // 1000
}

sync.RWMutex, salt okunur işlemler için RLock()/RUnlock() ile eşzamanlı okumaları optimize eder.

Yaygın Hatalar ve Çözümler

Go'daki eşzamanlılık klasik tuzaklar sunar. İşte en yaygın hatalar ve bunlardan nasıl kaçınılacağı.

common_mistakes.gogo
package main

import (
    "fmt"
    "sync"
)

func main() {
    // ERROR: Loop variable capture
    // All goroutines would print the same value
    for i := 0; i < 3; i++ {
        go func() {
            fmt.Println(i) // Capture by reference - BUG
        }()
    }

    // SOLUTION: Pass value as parameter
    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            fmt.Println(n) // Local copy - CORRECT
        }(i)
    }
    wg.Wait()

    // ERROR: Send on nil channel
    var ch chan int
    // ch <- 1 // Blocks forever

    // SOLUTION: Always initialize with make
    ch = make(chan int, 1)
    ch <- 1
    fmt.Println(<-ch)

    // ERROR: Send on closed channel
    done := make(chan bool)
    close(done)
    // done <- true // Panic!

    // SOLUTION: Check before send or use sync.Once
    select {
    case done <- true:
        fmt.Println("Sent")
    default:
        fmt.Println("Channel closed or full")
    }
}

Veri yarış algılama, derleme veya test sırasında -race bayrağını kullanır: go test -race ./....

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Sonuç

Go eşzamanlılığında ustalaşmak, iyi anlaşıldığında yüksek performanslı uygulamalar oluşturmaya olanak tanıyan birkaç temel kavrama dayanır.

Önemli noktalar:

✅ Goroutine'ler hafif ve ucuzdur - binlercesini oluşturmak kabul edilebilir kalır

✅ Channel'lar goroutine'ler arasında veri senkronize eder ve aktarır

select ifadesi birden fazla iletişimi ve zaman aşımını yönetir

✅ Worker pool deseni eşzamanlılığı sınırlar ve kaynakları optimize eder

context paketi iptal ve deadline'ları standartlaştırır

✅ Mutex'ler, channel'lar yetersiz kaldığında paylaşılan verileri korur

-race bayrağı testler sırasında veri yarışlarını algılar

"Belleği iletişim kurarak paylaşma" felsefesi, kilitli geleneksel çoklu iş parçacığından daha güvenli ve sürdürülebilir tasarımlara yönlendirir.

Etiketler

#go
#golang
#concurrency
#goroutines
#channels

Paylaş

İlgili makaleler