Skip to content

Commit fd80eb9

Browse files
author
Bryan C. Mills
committed
semaphore: add worker-pool example
I've commented several times in various forums that basically every time I've seen the “worker goroutine” pattern in Go, there has turned out to be a cleaner implementation using semaphores. This change adds a simple such example. (For more complex usage, I would generally pair the semaphore with an errgroup.Group.) Change-Id: Ibf69ee761d14ba59c1acc6a2d595b4fcf0d8f6d6 Reviewed-on: https://go-review.googlesource.com/75170 Reviewed-by: Ross Light <light@google.com>
1 parent 8e0aa68 commit fd80eb9

File tree

3 files changed

+98
-12
lines changed

3 files changed

+98
-12
lines changed

semaphore/semaphore_bench_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55
// +build go1.7
66

7-
package semaphore
7+
package semaphore_test
88

99
import (
1010
"fmt"
1111
"testing"
1212

1313
"golang.org/x/net/context"
14+
"golang.org/x/sync/semaphore"
1415
)
1516

1617
// weighted is an interface matching a subset of *Weighted. It allows
@@ -85,7 +86,7 @@ func BenchmarkNewSeq(b *testing.B) {
8586
for _, cap := range []int64{1, 128} {
8687
b.Run(fmt.Sprintf("Weighted-%d", cap), func(b *testing.B) {
8788
for i := 0; i < b.N; i++ {
88-
_ = NewWeighted(cap)
89+
_ = semaphore.NewWeighted(cap)
8990
}
9091
})
9192
b.Run(fmt.Sprintf("semChan-%d", cap), func(b *testing.B) {
@@ -116,7 +117,7 @@ func BenchmarkAcquireSeq(b *testing.B) {
116117
name string
117118
w weighted
118119
}{
119-
{"Weighted", NewWeighted(c.cap)},
120+
{"Weighted", semaphore.NewWeighted(c.cap)},
120121
{"semChan", newSemChan(c.cap)},
121122
} {
122123
b.Run(fmt.Sprintf("%s-acquire-%d-%d-%d", w.name, c.cap, c.size, c.N), func(b *testing.B) {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2017 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package semaphore_test
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"log"
11+
"runtime"
12+
13+
"golang.org/x/sync/semaphore"
14+
)
15+
16+
// Example_workerPool demonstrates how to use a semaphore to limit the number of
17+
// goroutines working on parallel tasks.
18+
//
19+
// This use of a semaphore mimics a typical “worker pool” pattern, but without
20+
// the need to explicitly shut down idle workers when the work is done.
21+
func Example_workerPool() {
22+
ctx := context.TODO()
23+
24+
var (
25+
maxWorkers = runtime.GOMAXPROCS(0)
26+
sem = semaphore.NewWeighted(int64(maxWorkers))
27+
out = make([]int, 32)
28+
)
29+
30+
// Compute the output using up to maxWorkers goroutines at a time.
31+
for i := range out {
32+
// When maxWorkers goroutines are in flight, Acquire blocks until one of the
33+
// workers finishes.
34+
if err := sem.Acquire(ctx, 1); err != nil {
35+
log.Printf("Failed to acquire semaphore: %v", err)
36+
break
37+
}
38+
39+
go func(i int) {
40+
defer sem.Release(1)
41+
out[i] = collatzSteps(i + 1)
42+
}(i)
43+
}
44+
45+
// Acquire all of the tokens to wait for any remaining workers to finish.
46+
//
47+
// If you are already waiting for the workers by some other means (such as an
48+
// errgroup.Group), you can omit this final Acquire call.
49+
if err := sem.Acquire(ctx, int64(maxWorkers)); err != nil {
50+
log.Printf("Failed to acquire semaphore: %v", err)
51+
}
52+
53+
fmt.Println(out)
54+
55+
// Output:
56+
// [0 1 7 2 5 8 16 3 19 6 14 9 9 17 17 4 12 20 20 7 7 15 15 10 23 10 111 18 18 18 106 5]
57+
}
58+
59+
// collatzSteps computes the number of steps to reach 1 under the Collatz
60+
// conjecture. (See https://en.wikipedia.org/wiki/Collatz_conjecture.)
61+
func collatzSteps(n int) (steps int) {
62+
if n <= 0 {
63+
panic("nonpositive input")
64+
}
65+
66+
for ; n > 1; steps++ {
67+
if steps < 0 {
68+
panic("too many steps")
69+
}
70+
71+
if n%2 == 0 {
72+
n /= 2
73+
continue
74+
}
75+
76+
const maxInt = int(^uint(0) >> 1)
77+
if n > (maxInt-1)/3 {
78+
panic("overflow")
79+
}
80+
n = 3*n + 1
81+
}
82+
83+
return steps
84+
}

semaphore/semaphore_test.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
package semaphore
5+
package semaphore_test
66

77
import (
88
"math/rand"
@@ -13,11 +13,12 @@ import (
1313

1414
"golang.org/x/net/context"
1515
"golang.org/x/sync/errgroup"
16+
"golang.org/x/sync/semaphore"
1617
)
1718

1819
const maxSleep = 1 * time.Millisecond
1920

20-
func HammerWeighted(sem *Weighted, n int64, loops int) {
21+
func HammerWeighted(sem *semaphore.Weighted, n int64, loops int) {
2122
for i := 0; i < loops; i++ {
2223
sem.Acquire(context.Background(), n)
2324
time.Sleep(time.Duration(rand.Int63n(int64(maxSleep/time.Nanosecond))) * time.Nanosecond)
@@ -30,7 +31,7 @@ func TestWeighted(t *testing.T) {
3031

3132
n := runtime.GOMAXPROCS(0)
3233
loops := 10000 / n
33-
sem := NewWeighted(int64(n))
34+
sem := semaphore.NewWeighted(int64(n))
3435
var wg sync.WaitGroup
3536
wg.Add(n)
3637
for i := 0; i < n; i++ {
@@ -51,15 +52,15 @@ func TestWeightedPanic(t *testing.T) {
5152
t.Fatal("release of an unacquired weighted semaphore did not panic")
5253
}
5354
}()
54-
w := NewWeighted(1)
55+
w := semaphore.NewWeighted(1)
5556
w.Release(1)
5657
}
5758

5859
func TestWeightedTryAcquire(t *testing.T) {
5960
t.Parallel()
6061

6162
ctx := context.Background()
62-
sem := NewWeighted(2)
63+
sem := semaphore.NewWeighted(2)
6364
tries := []bool{}
6465
sem.Acquire(ctx, 1)
6566
tries = append(tries, sem.TryAcquire(1))
@@ -83,7 +84,7 @@ func TestWeightedAcquire(t *testing.T) {
8384
t.Parallel()
8485

8586
ctx := context.Background()
86-
sem := NewWeighted(2)
87+
sem := semaphore.NewWeighted(2)
8788
tryAcquire := func(n int64) bool {
8889
ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond)
8990
defer cancel()
@@ -113,7 +114,7 @@ func TestWeightedDoesntBlockIfTooBig(t *testing.T) {
113114
t.Parallel()
114115

115116
const n = 2
116-
sem := NewWeighted(n)
117+
sem := semaphore.NewWeighted(n)
117118
{
118119
ctx, cancel := context.WithCancel(context.Background())
119120
defer cancel()
@@ -132,7 +133,7 @@ func TestWeightedDoesntBlockIfTooBig(t *testing.T) {
132133
})
133134
}
134135
if err := g.Wait(); err != nil {
135-
t.Errorf("NewWeighted(%v) failed to AcquireCtx(_, 1) with AcquireCtx(_, %v) pending", n, n+1)
136+
t.Errorf("semaphore.NewWeighted(%v) failed to AcquireCtx(_, 1) with AcquireCtx(_, %v) pending", n, n+1)
136137
}
137138
}
138139

@@ -143,7 +144,7 @@ func TestLargeAcquireDoesntStarve(t *testing.T) {
143144

144145
ctx := context.Background()
145146
n := int64(runtime.GOMAXPROCS(0))
146-
sem := NewWeighted(n)
147+
sem := semaphore.NewWeighted(n)
147148
running := true
148149

149150
var wg sync.WaitGroup

0 commit comments

Comments
 (0)