├── LICENSE ├── README.md ├── gate.go ├── gate_test.go ├── go.mod └── queue_example_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 The Go Authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Reference](https://pkg.go.dev/badge/github.com/neild/gate.svg)](https://pkg.go.dev/github.com/neild/gate) 2 | -------------------------------------------------------------------------------- /gate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 gate contains an alternative condition variable. 6 | // 7 | // A gate is a monitor (mutex + condition variable) with one bit of state. 8 | // 9 | // A gate exists in one of three states: 10 | // - locked 11 | // - unlocked and set 12 | // - unlocked and unset 13 | // 14 | // Lock operations may be unconditional, or wait for the condition to be set. 15 | // Unlock operations record the new state of the condition. 16 | // 17 | // Gates have several advantages over sync.Cond: 18 | // - Wait operations can be easily bounded by a context.Context lifetime. 19 | // - A Wait operation only returns successfully when the gate condition is set. 20 | // For example, if a gate's condition is set when a queue is non-empty, 21 | // then a successful return from Wait guarantees that an item is in the queue. 22 | // - No need to call Signal/Broadcast to notify waiters of a change in the condition. 23 | package gate 24 | 25 | import "context" 26 | 27 | // A gate is a monitor (mutex + condition variable) with one bit of state. 28 | type Gate struct { 29 | // When unlocked, exactly one of set or unset contains a value. 30 | // When locked, neither chan contains a value. 31 | set chan struct{} 32 | unset chan struct{} 33 | } 34 | 35 | // New returns a new, unlocked gate with the given condition state. 36 | func New(set bool) Gate { 37 | g := Gate{ 38 | set: make(chan struct{}, 1), 39 | unset: make(chan struct{}, 1), 40 | } 41 | g.Unlock(set) 42 | return g 43 | } 44 | 45 | // Lock acquires the gate unconditionally. 46 | // It reports whether the condition was set. 47 | func (g *Gate) Lock() (set bool) { 48 | // This doesn't take a Context parameter because 49 | // we don't expect unconditional lock operations to be time-bounded. 50 | select { 51 | case <-g.set: 52 | return true 53 | case <-g.unset: 54 | return false 55 | } 56 | } 57 | 58 | // WaitAndLock waits until the condition is set before acquiring the gate. 59 | // If the context expires, WaitAndLock returns an error and does not acquire the gate. 60 | func (g *Gate) WaitAndLock(ctx context.Context) error { 61 | // If the gate is available and the context is expired, 62 | // prefer locking the gate. 63 | select { 64 | case <-g.set: 65 | return nil 66 | default: 67 | } 68 | select { 69 | case <-g.set: 70 | return nil 71 | case <-ctx.Done(): 72 | return ctx.Err() 73 | } 74 | } 75 | 76 | // LockIfSet acquires the gate if and only if the condition is set. 77 | func (g *Gate) LockIfSet() (acquired bool) { 78 | select { 79 | case <-g.set: 80 | return true 81 | default: 82 | return false 83 | } 84 | } 85 | 86 | // Unlock sets the condition and releases the gate. 87 | func (g *Gate) Unlock(set bool) { 88 | if set { 89 | g.set <- struct{}{} 90 | } else { 91 | g.unset <- struct{}{} 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /gate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 gate_test 6 | 7 | import ( 8 | "context" 9 | "testing" 10 | "time" 11 | 12 | "github.com/neild/gate" 13 | ) 14 | 15 | func TestGateLockAndUnlock(t *testing.T) { 16 | g := gate.New(false) 17 | if set := g.Lock(); set { 18 | t.Errorf("g.Lock of never-locked gate: true, want false") 19 | } 20 | unlockedc := make(chan struct{}) 21 | donec := make(chan struct{}) 22 | go func() { 23 | defer close(donec) 24 | if set := g.Lock(); !set { 25 | t.Errorf("g.Lock of set gate: false, want true") 26 | } 27 | select { 28 | case <-unlockedc: 29 | default: 30 | t.Errorf("g.Lock succeeded while gate was held") 31 | } 32 | g.Unlock(false) 33 | }() 34 | time.Sleep(1 * time.Millisecond) 35 | close(unlockedc) 36 | g.Unlock(true) 37 | <-donec 38 | if set := g.Lock(); set { 39 | t.Errorf("g.Lock of unset gate: true, want false") 40 | } 41 | } 42 | 43 | func TestGateWaitAndLock(t *testing.T) { 44 | g := gate.New(false) 45 | // WaitAndLock is canceled. 46 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) 47 | defer cancel() 48 | if err := g.WaitAndLock(ctx); err != context.DeadlineExceeded { 49 | t.Fatalf("g.WaitAndLock = %v, want context.DeadlineExceeded", err) 50 | } 51 | // WaitAndLock succeeds. 52 | set := false 53 | go func() { 54 | time.Sleep(1 * time.Millisecond) 55 | g.Lock() 56 | set = true 57 | g.Unlock(true) 58 | }() 59 | if err := g.WaitAndLock(context.Background()); err != nil { 60 | t.Fatalf("g.WaitAndLock = %v, want nil", err) 61 | } 62 | if !set { 63 | t.Fatalf("g.WaitAndLock returned before gate was set") 64 | } 65 | g.Unlock(true) 66 | // WaitAndLock succeeds when the gate is set and the context is canceled. 67 | if err := g.WaitAndLock(ctx); err != nil { 68 | t.Fatalf("g.WaitAndLock = %v, want nil", err) 69 | } 70 | } 71 | 72 | func TestGateLockIfSet(t *testing.T) { 73 | g := gate.New(false) 74 | if locked := g.LockIfSet(); locked { 75 | t.Fatalf("g.LockIfSet of unset gate = %v, want false", locked) 76 | } 77 | g.Lock() 78 | if locked := g.LockIfSet(); locked { 79 | t.Fatalf("g.LockIfSet of locked gate = %v, want false", locked) 80 | } 81 | g.Unlock(true) 82 | if locked := g.LockIfSet(); !locked { 83 | t.Fatalf("g.LockIfSet of set gate = %v, want true", locked) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/neild/gate 2 | 3 | go 1.22 4 | -------------------------------------------------------------------------------- /queue_example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 gate_test 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "io" 11 | "slices" 12 | "time" 13 | 14 | "github.com/neild/gate" 15 | ) 16 | 17 | // A Queue is an unbounded queue of some item. 18 | type Queue[T any] struct { 19 | gate gate.Gate // set if queue is non-empty or closed 20 | err error 21 | q []T 22 | } 23 | 24 | // NewQueue returns a new queue. 25 | func NewQueue[T any]() *Queue[T] { 26 | return &Queue[T]{ 27 | gate: gate.New(false), 28 | } 29 | } 30 | 31 | // Close closes the queue, causing pending and future pop operations 32 | // to return immediately with err. 33 | func (q *Queue[T]) Close(err error) { 34 | q.gate.Lock() 35 | defer q.unlock() 36 | if q.err == nil { 37 | q.err = err 38 | } 39 | } 40 | 41 | // Put appends an item to the queue. 42 | // It returns true if the item was added, false if the queue is closed. 43 | func (q *Queue[T]) Put(v T) bool { 44 | q.gate.Lock() 45 | defer q.unlock() 46 | if q.err != nil { 47 | return false 48 | } 49 | q.q = append(q.q, v) 50 | return true 51 | } 52 | 53 | // Get removes the first item from the queue, blocking until ctx is done, an item is available, 54 | // or the queue is closed. 55 | func (q *Queue[T]) Get(ctx context.Context) (T, error) { 56 | var zero T 57 | if err := q.gate.WaitAndLock(ctx); err != nil { 58 | return zero, err 59 | } 60 | defer q.unlock() 61 | 62 | // WaitAndLock blocks until the gate condition is set, 63 | // so either the queue is closed (q.err != nil) or 64 | // there is at least one item in the queue. 65 | if q.err != nil { 66 | return zero, q.err 67 | } 68 | v := q.q[0] 69 | q.q = slices.Delete(q.q, 0, 1) 70 | return v, nil 71 | } 72 | 73 | // unlock unlocks the queue's gate, 74 | // setting the condition to true if the queue is non-empty or closed. 75 | func (q *Queue[T]) unlock() { 76 | q.gate.Unlock(q.err != nil || len(q.q) > 0) 77 | } 78 | 79 | func Example_queue() { 80 | q := NewQueue[int]() 81 | 82 | go func() { 83 | time.Sleep(1 * time.Millisecond) 84 | q.Put(1) 85 | time.Sleep(1 * time.Millisecond) 86 | q.Put(2) 87 | q.Close(io.EOF) 88 | }() 89 | 90 | fmt.Println(q.Get(context.Background())) 91 | fmt.Println(q.Get(context.Background())) 92 | fmt.Println(q.Get(context.Background())) 93 | // Output: 94 | // 1 95 | // 2 96 | // 0 EOF 97 | } 98 | --------------------------------------------------------------------------------