Go Channel Synchronization

Introduction

Channels in Go are not only used for communication between goroutines but also for synchronization. Channels can help coordinate the execution of goroutines, ensuring that tasks are performed in the correct order or that resources are properly managed. In this chapter, you will learn the various techniques for using channels to synchronize goroutines in Go, including basic synchronization, worker pools, and using select statements for advanced synchronization.

Basic Channel Synchronization

The simplest form of synchronization using channels is to signal when a goroutine has completed its work. An unbuffered channel is often used for this purpose because it blocks the sender until the receiver is ready and vice versa.

Example: Basic Synchronization

package main

import (
    "fmt"
)

func worker(done chan bool) {
    fmt.Println("Working...")
    // Simulate work
    done <- true // Signal that the work is done
}

func main() {
    done := make(chan bool)
    go worker(done)
    <-done // Wait for the worker to finish
    fmt.Println("Work done!")
}

In this example, the worker goroutine sends a signal on the done channel when it completes its work. The main goroutine waits for this signal before proceeding.

Using Channels to Synchronize Multiple Goroutines

You can use channels to synchronize the execution of multiple goroutines, ensuring that all goroutines complete their work before the main program exits.

Example: Waiting for Multiple Goroutines

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d started\n", id)
    // Simulate work
    fmt.Printf("Worker %d finished\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait() // Wait for all goroutines to finish
    fmt.Println("All workers done")
}

In this example, the sync.WaitGroup is used to wait for all worker goroutines to finish. The Add method increments the counter for each goroutine, and the Done method decrements it when a goroutine completes. The Wait method blocks until the counter is zero.

Using Buffered Channels for Task Coordination

Buffered channels can be used to coordinate tasks among goroutines, such as in a worker pool where tasks are distributed to multiple workers.

Example: Worker Pool

package main

import (
    "fmt"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("Worker %d started job %d\n", id, j)
        time.Sleep(time.Second) // Simulate work
        fmt.Printf("Worker %d finished job %d\n", id, j)
        results <- j * 2
    }
}

func main() {
    const numWorkers = 3
    const numJobs = 5

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    for w := 1; w <= numWorkers; w++ {
        go worker(w, jobs, results)
    }

    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    for a := 1; a <= numJobs; a++ {
        fmt.Println("Result:", <-results)
    }
}

In this example, jobs are sent to the jobs channel, and multiple worker goroutines receive jobs from this channel, process them, and send results to the results channel.

Using Select Statements for Advanced Synchronization

The select statement allows a goroutine to wait on multiple channel operations, enabling advanced synchronization patterns.

Example: Select with Timeout

package main

import (
    "fmt"
    "time"
)

func worker(done chan bool) {
    time.Sleep(2 * time.Second) // Simulate work
    done <- true
}

func main() {
    done := make(chan bool)
    go worker(done)

    select {
    case <-done:
        fmt.Println("Worker finished")
    case <-time.After(1 * time.Second):
        fmt.Println("Timeout waiting for worker")
    }
}

In this example, the select statement waits for either the worker to finish or a timeout to occur.

Conclusion

Channels in Go provide a powerful mechanism for synchronizing goroutines, ensuring proper coordination and execution order. By understanding how to use channels for basic synchronization, task coordination, and advanced synchronization with select statements, you can write more robust and efficient concurrent programs in Go.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top