summaryrefslogtreecommitdiffhomepage
path: root/pkg/sync/gate_unsafe.go
blob: ae32287ef0fb40495c770cb8512e6638402042fd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package sync

import (
	"fmt"
	"math"
	"sync/atomic"
	"unsafe"

	"gvisor.dev/gvisor/pkg/gohacks"
)

// Gate is a synchronization primitive that allows concurrent goroutines to
// "enter" it as long as it hasn't been closed yet. Once it's been closed,
// goroutines cannot enter it anymore, but are allowed to leave, and the closer
// will be informed when all goroutines have left.
//
// Gate is similar to WaitGroup:
//
// - Gate.Enter() is analogous to WaitGroup.Add(1), but may be called even if
// the Gate counter is 0 and fails if Gate.Close() has been called.
//
// - Gate.Leave() is equivalent to WaitGroup.Done().
//
// - Gate.Close() is analogous to WaitGroup.Wait(), but also causes future
// calls to Gate.Enter() to fail and may only be called once, from a single
// goroutine.
//
// This is useful, for example, in cases when a goroutine is trying to clean up
// an object for which multiple goroutines have pointers. In such a case, users
// would be required to enter and leave the Gate, and the cleaner would wait
// until all users are gone (and no new ones are allowed) before proceeding.
//
// Users:
//
//	if !g.Enter() {
//		// Gate is closed, we can't use the object.
//		return
//	}
//
//	// Do something with object.
//	[...]
//
//	g.Leave()
//
// Closer:
//
//	// Prevent new users from using the object, and wait for the existing
//	// ones to complete.
//	g.Close()
//
//	// Clean up the object.
//	[...]
//
type Gate struct {
	userCount int32
	closingG  uintptr
}

const preparingG = 1

// Enter tries to enter the gate. It will succeed if it hasn't been closed yet,
// in which case the caller must eventually call Leave().
//
// This function is thread-safe.
func (g *Gate) Enter() bool {
	if atomic.AddInt32(&g.userCount, 1) > 0 {
		return true
	}
	g.leaveAfterFailedEnter()
	return false
}

// leaveAfterFailedEnter is identical to Leave, but is marked noinline to
// prevent it from being inlined into Enter, since as of this writing inlining
// Leave into Enter prevents Enter from being inlined into its callers.
//go:noinline
func (g *Gate) leaveAfterFailedEnter() {
	if atomic.AddInt32(&g.userCount, -1) == math.MinInt32 {
		g.leaveClosed()
	}
}

// Leave leaves the gate. This must only be called after a successful call to
// Enter(). If the gate has been closed and this is the last one inside the
// gate, it will notify the closer that the gate is done.
//
// This function is thread-safe.
func (g *Gate) Leave() {
	if atomic.AddInt32(&g.userCount, -1) == math.MinInt32 {
		g.leaveClosed()
	}
}

func (g *Gate) leaveClosed() {
	if atomic.LoadUintptr(&g.closingG) == 0 {
		return
	}
	if g := atomic.SwapUintptr(&g.closingG, 0); g > preparingG {
		goready(g, 0)
	}
}

// Close closes the gate, causing future calls to Enter to fail, and waits
// until all goroutines that are currently inside the gate leave before
// returning.
//
// Only one goroutine can call this function.
func (g *Gate) Close() {
	if atomic.LoadInt32(&g.userCount) == math.MinInt32 {
		// The gate is already closed, with no goroutines inside. For legacy
		// reasons, we have to allow Close to be called again in this case.
		return
	}
	if v := atomic.AddInt32(&g.userCount, math.MinInt32); v == math.MinInt32 {
		// userCount was already 0.
		return
	} else if v >= 0 {
		panic("concurrent Close of sync.Gate")
	}

	if g := atomic.SwapUintptr(&g.closingG, preparingG); g != 0 {
		panic(fmt.Sprintf("invalid sync.Gate.closingG during Close: %#x", g))
	}
	if atomic.LoadInt32(&g.userCount) == math.MinInt32 {
		// The last call to Leave arrived while we were setting up closingG.
		return
	}
	// WaitReasonSemacquire/TraceEvGoBlockSync are consistent with WaitGroup.
	gopark(gateCommit, gohacks.Noescape(unsafe.Pointer(&g.closingG)), WaitReasonSemacquire, TraceEvGoBlockSync, 0)
}

//go:norace
//go:nosplit
func gateCommit(g uintptr, closingG unsafe.Pointer) bool {
	return RaceUncheckedAtomicCompareAndSwapUintptr((*uintptr)(closingG), preparingG, g)
}