Skip to content

Commit 58babf6

Browse files
mknyszekgopherbot
authored andcommitted
[release-branch.go1.23] runtime,time: use atomic.Int32 for isSending
This change switches isSending to be an atomic.Int32 instead of an atomic.Uint8. The Int32 version is managed as a counter, which is something that we couldn't do with Uint8 without adding a new intrinsic which may not be available on all architectures. That is, instead of only being able to support 8 concurrent timer firings on the same timer because we only have 8 independent bits to set for each concurrent timer firing, we can now have 2^31-1 concurrent timer firings before running into any issues. Like the fact that each bit-set was matched with a clear, here we match increments with decrements to indicate that we're in the "sending on a channel" critical section in the timer code, so we can report the correct result back on Stop or Reset. We choose an Int32 instead of a Uint32 because it's easier to check for obviously bad values (negative values are always bad) and 2^31-1 concurrent timer firings should be enough for anyone. Previously, we avoided anything bigger than a Uint8 because we could pack it into some padding in the runtime.timer struct. But it turns out that the type that actually matters, runtime.timeTimer, is exactly 96 bytes in size. This means its in the next size class up in the 112 byte size class because of an allocation header. We thus have some free space to work with. This change increases the size of this struct from 96 bytes to 104 bytes. (I'm not sure if runtime.timer is often allocated directly, but if it is, we get lucky in the same way too. It's exactly 80 bytes in size, which means its in the 96-byte size class, leaving us with some space to work with.) Fixes #69978 For #69969. Related to #69880 and #69312 and #69882. Change-Id: I9fd59cb6a69365c62971d1f225490a65c58f3e77 Cq-Include-Trybots: luci.golang.try:go1.23-linux-amd64-longtest Reviewed-on: https://go-review.googlesource.com/c/go/+/621616 Reviewed-by: Ian Lance Taylor <[email protected]> Auto-Submit: Michael Knyszek <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> (cherry picked from commit 6a49f81) Reviewed-on: https://go-review.googlesource.com/c/go/+/621856 Auto-Submit: Ian Lance Taylor <[email protected]> Reviewed-by: Michael Pratt <[email protected]>
1 parent 8d79bf7 commit 58babf6

File tree

2 files changed

+46
-43
lines changed

2 files changed

+46
-43
lines changed

src/runtime/time.go

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -30,36 +30,6 @@ type timer struct {
3030
state uint8 // state bits
3131
isChan bool // timer has a channel; immutable; can be read without lock
3232

33-
// isSending is used to handle races between running a
34-
// channel timer and stopping or resetting the timer.
35-
// It is used only for channel timers (t.isChan == true).
36-
// It is not used for tickers.
37-
// The lowest zero bit is set when about to send a value on the channel,
38-
// and cleared after sending the value.
39-
// The stop/reset code uses this to detect whether it
40-
// stopped the channel send.
41-
//
42-
// An isSending bit is set only when t.mu is held.
43-
// An isSending bit is cleared only when t.sendLock is held.
44-
// isSending is read only when both t.mu and t.sendLock are held.
45-
//
46-
// Setting and clearing Uint8 bits handles the case of
47-
// a timer that is reset concurrently with unlockAndRun.
48-
// If the reset timer runs immediately, we can wind up with
49-
// concurrent calls to unlockAndRun for the same timer.
50-
// Using matched bit set and clear in unlockAndRun
51-
// ensures that the value doesn't get temporarily out of sync.
52-
//
53-
// We use a uint8 to keep the timer struct small.
54-
// This means that we can only support up to 8 concurrent
55-
// runs of a timer, where a concurrent run can only occur if
56-
// we start a run, unlock the timer, the timer is reset to a new
57-
// value (or the ticker fires again), it is ready to run,
58-
// and it is actually run, all before the first run completes.
59-
// Since completing a run is fast, even 2 concurrent timer runs are
60-
// nearly impossible, so this should be safe in practice.
61-
isSending atomic.Uint8
62-
6333
blocked uint32 // number of goroutines blocked on timer's channel
6434

6535
// Timer wakes up at when, and then at when+period, ... (period > 0 only)
@@ -99,6 +69,20 @@ type timer struct {
9969
// sendLock protects sends on the timer's channel.
10070
// Not used for async (pre-Go 1.23) behavior when debug.asynctimerchan.Load() != 0.
10171
sendLock mutex
72+
73+
// isSending is used to handle races between running a
74+
// channel timer and stopping or resetting the timer.
75+
// It is used only for channel timers (t.isChan == true).
76+
// It is not used for tickers.
77+
// The value is incremented when about to send a value on the channel,
78+
// and decremented after sending the value.
79+
// The stop/reset code uses this to detect whether it
80+
// stopped the channel send.
81+
//
82+
// isSending is incremented only when t.mu is held.
83+
// isSending is decremented only when t.sendLock is held.
84+
// isSending is read only when both t.mu and t.sendLock are held.
85+
isSending atomic.Int32
10286
}
10387

10488
// init initializes a newly allocated timer t.
@@ -1065,20 +1049,11 @@ func (t *timer) unlockAndRun(now int64) {
10651049
}
10661050

10671051
async := debug.asynctimerchan.Load() != 0
1068-
var isSendingClear uint8
10691052
if !async && t.isChan && t.period == 0 {
10701053
// Tell Stop/Reset that we are sending a value.
1071-
// Set the lowest zero bit.
1072-
// We do this awkward step because atomic.Uint8
1073-
// doesn't support Add or CompareAndSwap.
1074-
// We only set bits with t locked.
1075-
v := t.isSending.Load()
1076-
i := sys.TrailingZeros8(^v)
1077-
if i == 8 {
1054+
if t.isSending.Add(1) < 0 {
10781055
throw("too many concurrent timer firings")
10791056
}
1080-
isSendingClear = 1 << i
1081-
t.isSending.Or(isSendingClear)
10821057
}
10831058

10841059
t.unlock()
@@ -1121,7 +1096,9 @@ func (t *timer) unlockAndRun(now int64) {
11211096
// We are committed to possibly sending a value
11221097
// based on seq, so no need to keep telling
11231098
// stop/modify that we are sending.
1124-
t.isSending.And(^isSendingClear)
1099+
if t.isSending.Add(-1) < 0 {
1100+
throw("mismatched isSending updates")
1101+
}
11251102
}
11261103

11271104
if t.seq != seq {

src/time/sleep_test.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -847,9 +847,9 @@ func testStopResetResultGODEBUG(t *testing.T, testStop bool, godebug string) {
847847
wg.Wait()
848848
}
849849

850-
// Test having a large number of goroutines wake up a timer simultaneously.
850+
// Test having a large number of goroutines wake up a ticker simultaneously.
851851
// This used to trigger a crash when run under x/tools/cmd/stress.
852-
func TestMultiWakeup(t *testing.T) {
852+
func TestMultiWakeupTicker(t *testing.T) {
853853
if testing.Short() {
854854
t.Skip("-short")
855855
}
@@ -872,6 +872,32 @@ func TestMultiWakeup(t *testing.T) {
872872
wg.Wait()
873873
}
874874

875+
// Test having a large number of goroutines wake up a timer simultaneously.
876+
// This used to trigger a crash when run under x/tools/cmd/stress.
877+
func TestMultiWakeupTimer(t *testing.T) {
878+
if testing.Short() {
879+
t.Skip("-short")
880+
}
881+
882+
goroutines := runtime.GOMAXPROCS(0)
883+
timer := NewTimer(Nanosecond)
884+
var wg sync.WaitGroup
885+
wg.Add(goroutines)
886+
for range goroutines {
887+
go func() {
888+
defer wg.Done()
889+
for range 10000 {
890+
select {
891+
case <-timer.C:
892+
default:
893+
}
894+
timer.Reset(Nanosecond)
895+
}
896+
}()
897+
}
898+
wg.Wait()
899+
}
900+
875901
// Benchmark timer latency when the thread that creates the timer is busy with
876902
// other work and the timers must be serviced by other threads.
877903
// https://golang.org/issue/38860

0 commit comments

Comments
 (0)