sync: make Cond MT-safe

This actually simplifies the code and avoids a heap allocation in the
call to Wait. Instead, it uses the Data field of the task to store
information on whether a task was signalled early.
This commit is contained in:
Ayke van Laethem
2024-11-04 09:41:26 +01:00
committed by Ron Evans
parent edb2f2a417
commit 6faf36fc64
+43 -36
View File
@@ -1,19 +1,26 @@
package sync package sync
import "internal/task" import (
"internal/task"
"unsafe"
)
// Condition variable.
// A goroutine that called Wait() can be in one of a few states depending on the
// Task.Data field:
// - When entering Wait, and before going to sleep, the data field is 0.
// - When the goroutine that calls Wait changes its data value from 0 to 1, it
// is going to sleep. It has not been awoken early.
// - When instead a call to Signal or Broadcast can change the data field from 0
// to 1, it will _not_ go to sleep but be signalled early.
// This can happen when a concurrent call to Signal happens, or the Unlock
// function calls Signal for some reason.
type Cond struct { type Cond struct {
L Locker L Locker
unlocking *earlySignal blocked task.Stack
blocked task.Stack lock task.PMutex
}
// earlySignal is a type used to implement a stack for signalling waiters while they are unlocking.
type earlySignal struct {
next *earlySignal
signaled bool
} }
func NewCond(l Locker) *Cond { func NewCond(l Locker) *Cond {
@@ -24,14 +31,14 @@ func (c *Cond) trySignal() bool {
// Pop a blocked task off of the stack, and schedule it if applicable. // Pop a blocked task off of the stack, and schedule it if applicable.
t := c.blocked.Pop() t := c.blocked.Pop()
if t != nil { if t != nil {
scheduleTask(t) dataPtr := (*task.Uint32)(unsafe.Pointer(&t.Data))
return true
}
// If there any tasks which are currently unlocking, signal one. // The data value is 0 when the task is not yet sleeping, and 1 when it is.
if c.unlocking != nil { if dataPtr.Swap(1) != 0 {
c.unlocking.signaled = true // The value was already 1, so the task went to sleep (or is about to go
c.unlocking = c.unlocking.next // to sleep). Schedule the task to be resumed.
scheduleTask(t)
}
return true return true
} }
@@ -40,21 +47,29 @@ func (c *Cond) trySignal() bool {
} }
func (c *Cond) Signal() { func (c *Cond) Signal() {
c.lock.Lock()
c.trySignal() c.trySignal()
c.lock.Unlock()
} }
func (c *Cond) Broadcast() { func (c *Cond) Broadcast() {
// Signal everything. // Signal everything.
c.lock.Lock()
for c.trySignal() { for c.trySignal() {
} }
c.lock.Unlock()
} }
func (c *Cond) Wait() { func (c *Cond) Wait() {
// Add an earlySignal frame to the stack so we can be signalled while unlocking. // Mark us as not yet signalled or sleeping.
early := earlySignal{ t := task.Current()
next: c.unlocking, dataPtr := (*task.Uint32)(unsafe.Pointer(&t.Data))
} dataPtr.Store(0)
c.unlocking = &early
// Add us to the list of waiting goroutines.
c.lock.Lock()
c.blocked.Push(t)
c.lock.Unlock()
// Temporarily unlock L. // Temporarily unlock L.
c.L.Unlock() c.L.Unlock()
@@ -63,22 +78,14 @@ func (c *Cond) Wait() {
defer c.L.Lock() defer c.L.Lock()
// If we were signaled while unlocking, immediately complete. // If we were signaled while unlocking, immediately complete.
if early.signaled { if dataPtr.Swap(1) != 0 {
// The data value was already 1, so we got a signal already (and weren't
// scheduled because trySignal was the first to change the value).
return return
} }
// Remove the earlySignal frame. // We were the first to change the value from 0 to 1, meaning we did not get
prev := c.unlocking // a signal during the call to Unlock(). So we wait until we do get a
for prev != nil && prev.next != &early { // signal.
prev = prev.next
}
if prev != nil {
prev.next = early.next
} else {
c.unlocking = early.next
}
// Wait for a signal.
c.blocked.Push(task.Current())
task.Pause() task.Pause()
} }