├── LICENSE
├── README.md
├── assets
└── logo.png
├── go.mod
├── item.go
├── no_copy.go
├── pool.go
└── pool_test.go
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 PJ Engineering and Business Solutions Pty. Ltd.
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 |
2 | ⭐ the project to show your appreciation. :arrow_upper_right:
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | # A Generic sync.Pool 
15 |
16 | This package is a **thin** wrapper over the `Pool` provided by the `sync` package. The `Pool` is an essential package to obtain maximum performance. It does so by reducing memory allocations.
17 |
18 | ## Extra Features
19 |
20 | - Invalidate an item from the Pool (so it never gets used again)
21 | - Set a maximum number of items for the Pool (Limit pool growth)
22 | - Return the number of items in the pool (idle and in-use)
23 | - Designed for concurrency
24 | - **Fully Generic**
25 |
26 | ## When should I use a pool?
27 |
28 | > If you frequently allocate many objects of the same type and you want to save some memory allocation and garbage allocation overhead — @jrv
29 |
30 | Read [How did I improve latency by 700% using sync.Pool](https://www.akshaydeo.com/blog/2017/12/23/How-did-I-improve-latency-by-700-percent-using-syncPool)
31 |
32 | ## Example
33 |
34 | ```go
35 | import "github.com/rocketlaunchr/go-pool"
36 |
37 | type X struct {}
38 |
39 | pool := pool.New(func() *X {
40 | return &X{}
41 | }, 5) // maximum of 5 items can be borrowed at a time
42 |
43 | borrowed := pool.Borrow()
44 | defer borrowed.Return()
45 |
46 | // Use item here or mark as invalid
47 | x := borrowed.Item() // Use Item of type: *X
48 | borrowed.MarkAsInvalid()
49 | ```
50 |
51 | Other useful packages
52 | ------------
53 |
54 | - [awesome-svelte](https://github.com/rocketlaunchr/awesome-svelte) - Resources for killing react
55 | - [dataframe-go](https://github.com/rocketlaunchr/dataframe-go) - Statistics and data manipulation
56 | - [dbq](https://github.com/rocketlaunchr/dbq) - Zero boilerplate database operations for Go
57 | - [electron-alert](https://github.com/rocketlaunchr/electron-alert) - SweetAlert2 for Electron Applications
58 | - [google-search](https://github.com/rocketlaunchr/google-search) - Scrape google search results
59 | - [igo](https://github.com/rocketlaunchr/igo) - A Go transpiler with cool new syntax such as fordefer (defer for for-loops)
60 | - [mysql-go](https://github.com/rocketlaunchr/mysql-go) - Properly cancel slow MySQL queries
61 | - [react](https://github.com/rocketlaunchr/react) - Build front end applications using Go
62 | - [remember-go](https://github.com/rocketlaunchr/remember-go) - Cache slow database queries
63 | - [testing-go](https://github.com/rocketlaunchr/testing-go) - Testing framework for unit testing
64 |
65 | ### Logo Credits
66 |
67 | 1. Renee French (gopher)
68 | 2. Samuel Jirénius (illustration)
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketlaunchr/go-pool/12f8d647803e163b2196a6c9c49aca69309898d9/assets/logo.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/rocketlaunchr/go-pool
2 |
3 | go 1.23.0
4 |
5 | require golang.org/x/sync v0.13.0
6 |
7 | retract v1.0.0 // Should not have been published
8 |
--------------------------------------------------------------------------------
/item.go:
--------------------------------------------------------------------------------
1 | package pool
2 |
3 | // ItemWrap wraps the item returned by the pool's factory.
4 | type ItemWrap[T any] interface {
5 | // Return returns the item back to the pool.
6 | Return()
7 |
8 | // MarkAsInvalid marks the item as invalid (eg. unusable, unstable or broken) so
9 | // that after it gets returned to the pool, it is discarded. It will eventually
10 | // get garbage collected.
11 | MarkAsInvalid()
12 |
13 | // Item represents the unwrapped item borrowed from the pool.
14 | Item() T
15 | }
16 |
17 | // itemWrap wraps the item returned by the pool's factory.
18 | type itemWrap[T any] struct {
19 | item T
20 |
21 | invalid bool
22 | pool interface{ returnItem(*itemWrap[T]) }
23 | }
24 |
25 | // Item represents the unwrapped item borrowed from the pool.
26 | func (iw *itemWrap[T]) Item() T {
27 | return iw.item
28 | }
29 |
30 | // Return returns the item back to the pool.
31 | func (iw *itemWrap[T]) Return() {
32 | iw.pool.returnItem(iw)
33 | }
34 |
35 | // reset restores iw to the zero value.
36 | func (iw *itemWrap[T]) reset() {
37 | iw.item = *new(T)
38 | iw.invalid = false
39 | iw.pool = nil
40 | }
41 |
42 | // MarkAsInvalid marks the item as invalid (eg. unusable, unstable or broken) so
43 | // that after it gets returned to the pool, it is discarded. It will eventually
44 | // get garbage collected.
45 | func (iw *itemWrap[T]) MarkAsInvalid() {
46 | iw.invalid = true
47 | }
48 |
--------------------------------------------------------------------------------
/no_copy.go:
--------------------------------------------------------------------------------
1 | package pool
2 |
3 | type noCopy struct{}
4 |
5 | func (*noCopy) Lock() {}
6 | func (*noCopy) Unlock() {}
7 |
--------------------------------------------------------------------------------
/pool.go:
--------------------------------------------------------------------------------
1 | package pool
2 |
3 | import (
4 | "context"
5 | "runtime"
6 | "sync"
7 | "sync/atomic"
8 |
9 | "golang.org/x/sync/semaphore"
10 | )
11 |
12 | // Options configures the Pool struct.
13 | type Options struct {
14 | // Initial creates an initial number of ready-to-use items in the pool.
15 | Initial uint32
16 |
17 | // Max represents the maximum number of items you can borrow at a time. This
18 | // prevents unbounded growth in the pool.
19 | //
20 | // Depending on the timing of Returns and Factory calls, the maximum number of
21 | // items in the pool can exceed Max by a small number for a short time.
22 | Max uint32
23 |
24 | // EnableCount, when set, enables the pool's Count function.
25 | //
26 | // NOTE: If you set this AND you need to set your own runtime Finalizer on the item,
27 | // wrap your item in another struct, with the Finalizer added to the inner item.
28 | EnableCount bool
29 | }
30 |
31 | // A Pool is a set of temporary objects that may be individually stored and
32 | // retrieved.
33 | //
34 | // Any item stored in the Pool may be removed automatically at any time without
35 | // notification. If the Pool holds the only reference when this happens, the
36 | // item might be deallocated.
37 | //
38 | // A Pool is safe for use by multiple goroutines simultaneously.
39 | //
40 | // Pool's purpose is to cache allocated but unused items for later reuse,
41 | // relieving pressure on the garbage collector. That is, it makes it easy to
42 | // build efficient, thread-safe free lists. However, it is not suitable for all
43 | // free lists.
44 | //
45 | // An appropriate use of a Pool is to manage a group of temporary items
46 | // silently shared among and potentially reused by concurrent independent
47 | // clients of a package. Pool provides a way to amortize allocation overhead
48 | // across many clients.
49 | //
50 | // The Pool scales under load and shrinks when quiescent.
51 | //
52 | // On the other hand, a free list maintained as part of a short-lived object is
53 | // not a suitable use for a Pool, since the overhead does not amortize well in
54 | // that scenario. It is more efficient to have such objects implement their own
55 | // free list.
56 | //
57 | // A Pool must not be copied after first use.
58 | type Pool[T any] struct {
59 | noCopy noCopy
60 |
61 | initial *uint32 // if nil, then initial items have already been created (or initial option was no set)
62 |
63 | itemWrapPool *sync.Pool
64 | syncPool sync.Pool
65 | semMax *semaphore.Weighted
66 |
67 | enableCount bool
68 | count uint32 // count keeps track of approximately how many items are in existence (in the pool and in-use).
69 | countBorrowedOut uint32
70 | }
71 |
72 | // New creates a new Pool.
73 | //
74 | // factory specifies a function to generate a new item when Borrow is called. opts accepts either an int (representing the max)
75 | // or an Options struct.
76 | //
77 | // NOTE: factory should generally only return pointer types, since a pointer can be put into the return interface
78 | // value without an allocation.
79 | func New[T any](factory func() T, opts ...any) Pool[T] {
80 | pool := Pool[T]{
81 | itemWrapPool: &sync.Pool{
82 | New: func() any { return new(itemWrap[T]) },
83 | },
84 | }
85 | if len(opts) == 0 {
86 | pool.syncPool.New = func() any { return factory() }
87 | return pool
88 | }
89 | switch o := opts[0].(type) {
90 | case int:
91 | pool.semMax = semaphore.NewWeighted(int64(o))
92 | case uint32:
93 | pool.semMax = semaphore.NewWeighted(int64(o))
94 | case int64:
95 | pool.semMax = semaphore.NewWeighted(o)
96 | case Options:
97 | // max
98 | if o.Max != 0 {
99 | if o.Initial > o.Max {
100 | panic("Initial must not exceed Max")
101 | }
102 | pool.semMax = semaphore.NewWeighted(int64(o.Max))
103 | }
104 |
105 | // initial
106 | if o.Initial > 0 {
107 | pool.initial = &o.Initial
108 | }
109 |
110 | // enableCount
111 | pool.enableCount = o.EnableCount
112 | default:
113 | panic("opts must be an int or Options struct")
114 | }
115 |
116 | pool.syncPool.New = func() any {
117 | newItem := factory()
118 | if pool.enableCount {
119 | atomic.AddUint32(&pool.count, 1) // pool.count++
120 | runtime.SetFinalizer(newItem, func(newItem any) {
121 | atomic.AddUint32(&pool.count, ^uint32(0)) // pool.count--
122 | })
123 | }
124 | return newItem
125 | }
126 |
127 | if pool.initial != nil {
128 | // create initial number of items
129 | items := make([]*itemWrap[T], 0, *pool.initial)
130 |
131 | // create new items
132 | for i := uint32(0); i < *pool.initial; i++ {
133 | items = append(items, pool.borrow())
134 | }
135 | // return new items
136 | for j := len(items) - 1; j >= 0; j-- {
137 | pool.returnItem(items[j])
138 | }
139 | pool.initial = nil
140 | }
141 |
142 | return pool
143 | }
144 |
145 | func (p *Pool[T]) borrow() *itemWrap[T] {
146 | if p.semMax != nil {
147 | p.semMax.Acquire(context.Background(), 1)
148 | }
149 | if p.enableCount {
150 | atomic.AddUint32(&p.countBorrowedOut, 1) // p.countBorrowedOut++
151 | }
152 |
153 | wrap := p.itemWrapPool.Get().(*itemWrap[T])
154 | item := p.syncPool.Get()
155 |
156 | wrap.item = item.(T)
157 | wrap.pool = p
158 | return wrap
159 | }
160 |
161 | func (p *Pool[T]) returnItem(wrap *itemWrap[T]) {
162 | if !wrap.invalid {
163 | p.syncPool.Put(wrap.item)
164 | }
165 | wrap.reset()
166 |
167 | p.itemWrapPool.Put(wrap)
168 | if p.enableCount {
169 | atomic.AddUint32(&p.countBorrowedOut, ^uint32(0)) // p.countBorrowedOut--
170 | }
171 | if p.semMax != nil {
172 | p.semMax.Release(1)
173 | }
174 | }
175 |
176 | // Borrow obtains an item from the pool.
177 | // If the Max option is set, then this function will
178 | // block until an item is returned back into the pool.
179 | //
180 | // After the item is no longer required, you must call
181 | // Return on the item.
182 | func (p *Pool[T]) Borrow() ItemWrap[T] {
183 | return p.borrow()
184 | }
185 |
186 | // ReturnItem returns an item back to the pool. However, the
187 | // recommended approach is to call Return on the ItemWrap.
188 | func (p *Pool[T]) ReturnItem(x ItemWrap[T]) {
189 | p.returnItem(x.(*itemWrap[T]))
190 | }
191 |
192 | // Count returns approximately the number of items in the pool (idle).
193 | // If you want an accurate number, call runtime.GC() twice before calling Count (not recommended).
194 | //
195 | // NOTE: Count can exceed both the Initial and Max value by a small number for a short time.
196 | func (p *Pool[T]) Count() uint32 {
197 | if !p.enableCount {
198 | return 0
199 | }
200 | c := atomic.LoadUint32(&p.count)
201 | b := atomic.LoadUint32(&p.countBorrowedOut)
202 | if c > b {
203 | return c - b
204 | }
205 | return 0
206 | }
207 |
208 | // OnLoan returns how many items are in-use.
209 | func (p *Pool[T]) OnLoan() uint32 {
210 | if !p.enableCount {
211 | return 0
212 | }
213 | return atomic.LoadUint32(&p.countBorrowedOut)
214 | }
215 |
--------------------------------------------------------------------------------
/pool_test.go:
--------------------------------------------------------------------------------
1 | package pool_test
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "testing"
7 |
8 | pool "github.com/rocketlaunchr/go-pool"
9 | )
10 |
11 | func TestPool(t *testing.T) {
12 | type X struct {
13 | ID string
14 | }
15 |
16 | randoms := []string{"AAA", "BBB", "CCC", "DDD", "EEE", "FFF", "GGG", "HHH", "III", "JJJ", "KKK", "LLL", "MMM", "NNN"}
17 | ids := map[string]struct{}{}
18 | for _, v := range randoms {
19 | ids[v] = struct{}{}
20 | }
21 |
22 | i := 0
23 | p := pool.New(func() *X {
24 | // This function will panic if Max field is not able to restrict growth
25 | // of pool and slice goes "index out of range"
26 | defer func() {
27 | i++
28 | }()
29 | return &X{randoms[i]}
30 | }, pool.Options{Max: 1})
31 |
32 | for i := 0; i < 1_000_000; i++ {
33 | i := i
34 | t.Run(fmt.Sprintf("test %v", i), func(t *testing.T) {
35 | t.Parallel()
36 | borrowed := p.Borrow()
37 | defer borrowed.Return()
38 |
39 | id := borrowed.Item().ID
40 |
41 | if _, exists := ids[id]; !exists {
42 | t.Errorf("Result was incorrect, got: %s, want: %s.", id, strings.Join(randoms, ","))
43 | }
44 | })
45 | }
46 | }
47 |
--------------------------------------------------------------------------------