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.