Simple Semaphore With a Buffered Channel in Go
You can pretty easily use buffered channels as a semaphore to simulate a worker pool in Go.
Buffered Channels
Let’s do a quick review of buffered channels.
- With unbuffered channels, both sends and receives block execution. They will only accept sends (
chan <-
) if there is a corresponding receive (<- chan
) ready to receive the sent value (source). - With a buffered channel, it will accept a number of values without a receiver waiting, up until the capacity of the buffer.
I found this example from Naveen Ramanathan to be very helpful (source here).
1package main2
3import (4 "fmt"5 "time"6)7
8func write(ch chan int) {9 for i := 0; i < 5; i++ {10 ch <- i11 fmt.Println("successfully wrote", i, "to ch")12 }13 close(ch)14}15
16func main() {17 ch := make(chan int, 2)18 go write(ch)19 time.Sleep(2 * time.Second)20 for v := range ch {21 fmt.Println("read value", v,"from ch")22 time.Sleep(2 * time.Second)23 }24}
successfully wrote 0 to chsuccessfully wrote 1 to chread value 0 from chsuccessfully wrote 2 to chread value 1 from chsuccessfully wrote 3 to chread value 2 from chsuccessfully wrote 4 to chread value 3 from chread value 4 from ch
Example
To acquire the semaphore, you send to the buffered channel. To release the semaphore, you receive from the buffered channel.
The data actually stored in the buffered channel doesn’t really matter.
1package main2
3type Semaphore struct {4 bc chan struct{}5}6
7func NewSemaphore(n int) *Semaphore {8 return &Semaphore{bc: make(chan struct{}, n)}9}10
11func (s *Semaphore) Acquire() {12 s.bc <- struct{}{}13}14
15func (s *Semaphore) Release() {16 <-s.bc17}
Then use it:
1package main2
3import (4 "fmt"5 "time"6)7
8func main() {9 s := NewSemaphore(3)10
11 for i := 0; i < 20; i++ {12 s.Acquire()13 go func(i int) {14 defer s.Release()15 expensiveThing(i)16 }(i)17 }18
19}20
21func expensiveThing(i int) {22 time.Sleep(1 * time.Second)23
24 fmt.Println("Expensive thing done", i)25}
There is, however, a problem here. The semaphore constrains the number of goroutines that are doing the expensive thing at once, but it doesn’t make the main goroutine wait for all the worker goroutines to finish.
In fact if you watch the output gif closely, you’ll realize it doesn’t print out “Expensive thing done 18” (or 19).
Let’s fix that.
1package main2
3import (4 "fmt"5 "sync"6 "time"7)8
9func main() {10 s := NewSemaphore(3)11 wg := sync.WaitGroup{}12
13 for i := 0; i < 20; i++ {14 wg.Add(1)15 s.Acquire()16 go func(i int) {17 defer wg.Done()18 defer s.Release()19 expensiveThing(i)20 }(i)21 }22
23 wg.Wait()24}25
26func expensiveThing(i int) {27 time.Sleep(1 * time.Second)28
29 fmt.Println("Expensive thing done", i)30}
Wow! You read the whole thing. People who make it this far sometimes
want to receive emails when I post something new.
I also have an RSS feed.