├── .github ├── dependabot.yml └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── example └── main.go ├── go.mod ├── pool.go ├── pool_test.go └── time.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: gomod 5 | directory: / 6 | labels: 7 | - dependencies 8 | schedule: 9 | interval: daily 10 | 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | labels: 14 | - dependencies 15 | schedule: 16 | interval: "daily" 17 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: 1.18 19 | 20 | - name: Build 21 | run: go build -v ./... 22 | 23 | - name: Test 24 | run: go test -race -coverprofile=coverage.txt -covermode=atomic 25 | 26 | - name: Upload coverage reports to Codecov 27 | uses: codecov/codecov-action@v5 28 | with: 29 | token: ${{ secrets.CODECOV_TOKEN }} 30 | slug: sysulq/goroutine-pool 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sophos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goroutine-pool 2 | [![Go](https://github.com/sysulq/goroutine-pool/actions/workflows/go.yml/badge.svg)](https://github.com/sysulq/goroutine-pool/actions/workflows/go.yml) 3 | [![Coverage](https://codecov.io/gh/sysulq/goroutine-pool/branch/master/graph/badge.svg)](https://codecov.io/gh/sysulq/goroutine-pool) 4 | 5 | A simple goroutine pool which can create and release goroutine dynamically, inspired by fasthttp. 6 | 7 | # install 8 | ``` 9 | go get -u -v github.com/sysulq/goroutine-pool 10 | ``` 11 | # example 12 | ``` 13 | package main 14 | 15 | import ( 16 | "fmt" 17 | pool "github.com/sysulq/goroutine-pool" 18 | "sync" 19 | ) 20 | 21 | func main() { 22 | wg := sync.WaitGroup{} 23 | for i := 0; i < 5; i++ { 24 | wg.Add(1) 25 | pool.Go(func() { 26 | fmt.Println(i) 27 | wg.Done() 28 | }) 29 | } 30 | wg.Wait() 31 | fmt.Println() 32 | 33 | for i := 0; i < 5; i++ { 34 | wg.Add(1) 35 | n := i 36 | pool.Go(func() { 37 | fmt.Println(n) 38 | wg.Done() 39 | }) 40 | } 41 | wg.Wait() 42 | } 43 | ``` 44 | 45 | ``` 46 | $ go run main.go 47 | 5 48 | 5 49 | 5 50 | 5 51 | 5 52 | 53 | 4 54 | 1 55 | 0 56 | 2 57 | 3 58 | ``` 59 | 60 | # benchmarks 61 | 62 | ## With Wait 63 | ``` 64 | Running tool: D:\Go\bin\go.exe test -benchmem -run=^$ workerpool -bench ^BenchmarkGoroutine$ 65 | 66 | goos: windows 67 | goarch: amd64 68 | pkg: workerpool 69 | BenchmarkGoroutine-4 1000 1634621 ns/op 65 B/op 1 allocs/op 70 | PASS 71 | ok workerpool 2.047s 72 | Success: Benchmarks passed. 73 | ``` 74 | 75 | ``` 76 | Running tool: D:\Go\bin\go.exe test -benchmem -run=^$ workerpool -bench ^BenchmarkPool$ 77 | 78 | goos: windows 79 | goarch: amd64 80 | pkg: workerpool 81 | BenchmarkPool-4 2000 1146818 ns/op 17 B/op 1 allocs/op 82 | PASS 83 | ok workerpool 2.702s 84 | Success: Benchmarks passed. 85 | ``` 86 | 87 | ## Without Wait 88 | 89 | ### cpu run at 100% 90 | ``` 91 | Running tool: D:\Go\bin\go.exe test -benchmem -run=^$ workerpool -bench ^BenchmarkGoroutineWithoutWait$ 92 | 93 | goos: windows 94 | goarch: amd64 95 | pkg: workerpool 96 | BenchmarkGoroutineWithoutWait-4 1000000 4556 ns/op 517 B/op 1 allocs/op 97 | PASS 98 | ok workerpool 5.649s 99 | Success: Benchmarks passed. 100 | ``` 101 | 102 | ### cpu relatively low 103 | 104 | ``` 105 | Running tool: D:\Go\bin\go.exe test -benchmem -run=^$ workerpool -bench ^BenchmarkPoolWithoutWait$ 106 | 107 | goos: windows 108 | goarch: amd64 109 | pkg: workerpool 110 | BenchmarkPoolWithoutWait-4 10000000 144 ns/op 3 B/op 0 allocs/op 111 | PASS 112 | ok workerpool 4.812s 113 | Success: Benchmarks passed. 114 | ``` 115 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | pool "github.com/sysulq/goroutine-pool" 8 | ) 9 | 10 | func main() { 11 | wg := sync.WaitGroup{} 12 | for i := 0; i < 5; i++ { 13 | wg.Add(1) 14 | pool.Go(func() { 15 | fmt.Println(i) 16 | wg.Done() 17 | }) 18 | } 19 | wg.Wait() 20 | fmt.Println() 21 | 22 | for i := 0; i < 5; i++ { 23 | wg.Add(1) 24 | n := i 25 | pool.Go(func() { 26 | fmt.Println(n) 27 | wg.Done() 28 | }) 29 | } 30 | wg.Wait() 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sysulq/goroutine-pool 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // WorkerPool serves outgoing connections via a pool of workers 10 | // in FILO order, i.e. the most recently stopped worker will serve the next 11 | // incoming connection. 12 | // 13 | // Such a scheme keeps CPU caches hot (in theory). 14 | type WorkerPool struct { 15 | MaxWorkersCount int 16 | 17 | MaxIdleWorkerDuration time.Duration 18 | 19 | lock sync.Mutex 20 | workersCount int 21 | mustStop bool 22 | 23 | ready []*workerChan 24 | 25 | stopCh chan struct{} 26 | 27 | workerChanPool sync.Pool 28 | } 29 | 30 | type workerChan struct { 31 | lastUseTime time.Time 32 | ch chan func() 33 | } 34 | 35 | var ( 36 | once sync.Once 37 | workerpool *WorkerPool 38 | ) 39 | 40 | func init() { 41 | once.Do(func() { 42 | workerpool = &WorkerPool{ 43 | MaxWorkersCount: 256 * 1024, 44 | MaxIdleWorkerDuration: 10 * time.Second, 45 | } 46 | workerpool.Start() 47 | }) 48 | } 49 | 50 | func (wp *WorkerPool) Start() { 51 | if wp.stopCh != nil { 52 | panic("BUG: WorkerPool already started") 53 | } 54 | wp.stopCh = make(chan struct{}) 55 | if wp.MaxWorkersCount == 0 { 56 | // 默认 57 | wp.MaxWorkersCount = 256 * 1024 58 | } 59 | stopCh := wp.stopCh 60 | go func() { 61 | var scratch []*workerChan 62 | for { 63 | wp.clean(&scratch) 64 | select { 65 | case <-stopCh: 66 | return 67 | default: 68 | time.Sleep(wp.getMaxIdleWorkerDuration()) 69 | } 70 | } 71 | }() 72 | } 73 | 74 | func (wp *WorkerPool) Stop() { 75 | if wp.stopCh == nil { 76 | panic("BUG: WorkerPool wasn't started") 77 | } 78 | close(wp.stopCh) 79 | wp.stopCh = nil 80 | 81 | // Stop all the workers waiting for incoming connections. 82 | // Do not wait for busy workers - they will stop after 83 | // serving the connection and noticing wp.mustStop = true. 84 | wp.lock.Lock() 85 | ready := wp.ready 86 | for i, ch := range ready { 87 | ch.ch <- nil 88 | ready[i] = nil 89 | } 90 | wp.ready = ready[:0] 91 | wp.mustStop = true 92 | wp.lock.Unlock() 93 | } 94 | 95 | func (wp *WorkerPool) getMaxIdleWorkerDuration() time.Duration { 96 | if wp.MaxIdleWorkerDuration <= 0 { 97 | return 10 * time.Second 98 | } 99 | return wp.MaxIdleWorkerDuration 100 | } 101 | 102 | func (wp *WorkerPool) clean(scratch *[]*workerChan) { 103 | maxIdleWorkerDuration := wp.getMaxIdleWorkerDuration() 104 | 105 | // Clean least recently used workers if they didn't serve connections 106 | // for more than maxIdleWorkerDuration. 107 | currentTime := time.Now() 108 | 109 | wp.lock.Lock() 110 | ready := wp.ready 111 | n := len(ready) 112 | i := 0 113 | for i < n && currentTime.Sub(ready[i].lastUseTime) > maxIdleWorkerDuration { 114 | i++ 115 | } 116 | *scratch = append((*scratch)[:0], ready[:i]...) 117 | if i > 0 { 118 | m := copy(ready, ready[i:]) 119 | for i = m; i < n; i++ { 120 | ready[i] = nil 121 | } 122 | wp.ready = ready[:m] 123 | } 124 | wp.lock.Unlock() 125 | 126 | // Notify obsolete workers to stop. 127 | // This notification must be outside the wp.lock, since ch.ch 128 | // may be blocking and may consume a lot of time if many workers 129 | // are located on non-local CPUs. 130 | tmp := *scratch 131 | for i, ch := range tmp { 132 | ch.ch <- nil 133 | tmp[i] = nil 134 | } 135 | } 136 | 137 | func Go(c func()) { workerpool.Go(c) } 138 | func (wp *WorkerPool) Go(c func()) bool { 139 | ch := wp.getCh() 140 | if ch == nil { 141 | return false 142 | } 143 | ch.ch <- c 144 | return true 145 | } 146 | 147 | var workerChanCap = func() int { 148 | // Use blocking workerChan if GOMAXPROCS=1. 149 | // This immediately switches Serve to WorkerFunc, which results 150 | // in higher performance (under go1.5 at least). 151 | if runtime.GOMAXPROCS(0) == 1 { 152 | return 0 153 | } 154 | 155 | // Use non-blocking workerChan if GOMAXPROCS>1, 156 | // since otherwise the Serve caller (Acceptor) may lag accepting 157 | // new connections if WorkerFunc is CPU-bound. 158 | return 1 159 | }() 160 | 161 | func (wp *WorkerPool) getCh() *workerChan { 162 | var ch *workerChan 163 | createWorker := false 164 | 165 | wp.lock.Lock() 166 | ready := wp.ready 167 | n := len(ready) - 1 168 | if n < 0 { 169 | if wp.workersCount < wp.MaxWorkersCount { 170 | createWorker = true 171 | wp.workersCount++ 172 | } 173 | } else { 174 | ch = ready[n] 175 | ready[n] = nil 176 | wp.ready = ready[:n] 177 | } 178 | wp.lock.Unlock() 179 | 180 | if ch == nil { 181 | if !createWorker { 182 | return nil 183 | } 184 | vch := wp.workerChanPool.Get() 185 | if vch == nil { 186 | vch = &workerChan{ 187 | ch: make(chan func(), workerChanCap), 188 | } 189 | } 190 | ch = vch.(*workerChan) 191 | go func() { 192 | wp.workerFunc(ch) 193 | wp.workerChanPool.Put(vch) 194 | }() 195 | } 196 | return ch 197 | } 198 | 199 | func (wp *WorkerPool) release(ch *workerChan) bool { 200 | ch.lastUseTime = coarseTimeNow() 201 | wp.lock.Lock() 202 | if wp.mustStop { 203 | wp.lock.Unlock() 204 | return false 205 | } 206 | wp.ready = append(wp.ready, ch) 207 | wp.lock.Unlock() 208 | return true 209 | } 210 | 211 | func (wp *WorkerPool) workerFunc(ch *workerChan) { 212 | var c func() 213 | 214 | for c = range ch.ch { 215 | if c == nil { 216 | break 217 | } 218 | 219 | c() 220 | 221 | if !wp.release(ch) { 222 | break 223 | } 224 | } 225 | 226 | wp.lock.Lock() 227 | wp.workersCount-- 228 | wp.lock.Unlock() 229 | } 230 | -------------------------------------------------------------------------------- /pool_test.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestCreate(t *testing.T) { 12 | for i := 0; i < 10; i++ { 13 | Go(func() { 14 | fmt.Println("hello") 15 | }) 16 | } 17 | } 18 | 19 | func TestStop(t *testing.T) { 20 | for i := 0; i < 10; i++ { 21 | Go(func() { 22 | fmt.Println("hello") 23 | }) 24 | } 25 | } 26 | 27 | func BenchmarkPool(b *testing.B) { 28 | num := 10000 29 | 30 | wg := sync.WaitGroup{} 31 | 32 | for i := 0; i < num; i++ { 33 | wg.Add(1) 34 | Go(func() { 35 | wg.Done() 36 | }) 37 | } 38 | wg.Wait() 39 | b.ResetTimer() 40 | 41 | for k := 0; k < b.N; k++ { 42 | wg.Add(1) 43 | // assert.True(b, wp.Serve(func() { 44 | Go(func() { 45 | rand.Int() 46 | time.Sleep(10 * time.Microsecond) 47 | wg.Done() 48 | }) 49 | wg.Wait() 50 | } 51 | } 52 | 53 | func BenchmarkGoroutine(b *testing.B) { 54 | b.ResetTimer() 55 | 56 | wg := sync.WaitGroup{} 57 | for k := 0; k < b.N; k++ { 58 | wg.Add(1) 59 | go func() { 60 | rand.Int() 61 | time.Sleep(10 * time.Microsecond) 62 | wg.Done() 63 | }() 64 | wg.Wait() 65 | } 66 | } 67 | 68 | func BenchmarkPoolWithoutWait(b *testing.B) { 69 | num := 10000 70 | 71 | wg := sync.WaitGroup{} 72 | 73 | for i := 0; i < num; i++ { 74 | wg.Add(1) 75 | Go(func() { 76 | wg.Done() 77 | }) 78 | } 79 | wg.Wait() 80 | b.ResetTimer() 81 | 82 | for k := 0; k < b.N; k++ { 83 | // wg.Add(1) 84 | // assert.True(b, wp.Serve(func() { 85 | Go(func() { 86 | rand.Int() 87 | time.Sleep(1 * time.Microsecond) 88 | // wg.Done() 89 | }) 90 | // wg.Wait() 91 | } 92 | } 93 | 94 | func BenchmarkGoroutineWithoutWait(b *testing.B) { 95 | b.ResetTimer() 96 | 97 | // wg := sync.WaitGroup{} 98 | for k := 0; k < b.N; k++ { 99 | // wg.Add(1) 100 | go func() { 101 | rand.Int() 102 | time.Sleep(1 * time.Microsecond) 103 | // wg.Done() 104 | }() 105 | // wg.Wait() 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | ) 7 | 8 | // coarseTimeNow returns the current time truncated to the nearest second. 9 | // 10 | // This is a faster alternative to time.Now(). 11 | func coarseTimeNow() time.Time { 12 | tp := coarseTime.Load().(*time.Time) 13 | return *tp 14 | } 15 | 16 | func init() { 17 | t := time.Now().Truncate(time.Second) 18 | coarseTime.Store(&t) 19 | go func() { 20 | for { 21 | time.Sleep(time.Second) 22 | t := time.Now().Truncate(time.Second) 23 | coarseTime.Store(&t) 24 | } 25 | }() 26 | } 27 | 28 | var coarseTime atomic.Value 29 | --------------------------------------------------------------------------------