pysync

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 4, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package pysync ports the synchronization primitives in cpython/Python/lock.c, parking_lot.c, and critical_section.c.

The package is named pysync (not sync) because the primitives have semantics distinct from Go's standard sync package: byte-flag PyMutex with parked-waiter bits, address-keyed parking, PEP 703 critical sections, and so on.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AfterFork

func AfterFork()

AfterFork resets the parking lot. In CPython this is required after fork() because parked waiters belong to threads that no longer exist in the child. Go programs do not POSIX-fork the runtime, so the function is preserved for source-shape parity but is a no-op in practice. Calling it on a live process would deadlock waiters; only call it when the program is single-threaded.

CPython: Python/parking_lot.c:L434 _PyParkingLot_AfterFork

func Unpark

func Unpark(addr unsafe.Pointer, fn UnparkFn)

Unpark wakes one waiter parked on addr. fn is called while the bucket lock is held; the waiter is signaled after the lock is released. If no waiter is queued, fn is called with (nil, false).

CPython: Python/parking_lot.c:L393 _PyParkingLot_Unpark

func UnparkAll

func UnparkAll(addr unsafe.Pointer)

UnparkAll wakes every waiter parked on addr.

CPython: Python/parking_lot.c:L416 _PyParkingLot_UnparkAll

Types

type CSThread

type CSThread struct {
	// contains filtered or unexported fields
}

CSThread holds the critical-section stack head for a single thread of execution. Pass the same value to every Begin and End call from the goroutine that owns it.

CPython: Python/critical_section.c:L95 _PyCriticalSection_SuspendAll (adapted from)

func (*CSThread) Top

func (t *CSThread) Top() *CriticalSection

Top returns the innermost critical section, or nil if none is active.

CPython: Python/critical_section.c:L95 _PyCriticalSection_SuspendAll (adapted from)

type CriticalSection

type CriticalSection struct {
	// contains filtered or unexported fields
}

CriticalSection mirrors PyCriticalSection (PEP 703).

The full design serializes access to a Python object across threads when the GIL is disabled. In a GIL build (the default and the only build gopy supports in v0.1) the GIL already serializes, so Begin and End reduce to bookkeeping: push and pop a per-thread linked stack. v0.14 will turn the bookkeeping into real mutex acquisitions when the pygil_disabled tag is enabled.

The per-thread stack head lives in CSThread for now because gopy has no goroutine-local thread state. v0.7 (state package) will fold this into the real thread state.

CPython: Include/cpython/critical_section.h:L108 PyCriticalSection

func (*CriticalSection) Begin

func (c *CriticalSection) Begin(t *CSThread, m *Mutex)

Begin starts a critical section guarded by m. In v0.1 (GIL build) this only pushes onto the per-thread stack. v0.14 will acquire m.

CPython: Python/critical_section.c:L21 _PyCriticalSection_BeginSlow

func (*CriticalSection) BeginMutex2

func (c *CriticalSection) BeginMutex2(t *CSThread, m1, m2 *Mutex)

BeginMutex2 starts a critical section guarding two mutexes. The acquisition order is fixed by m1 then m2 to prevent deadlocks between callers that touch the same pair, mirroring _PyCriticalSection2 in C.

CPython: Python/critical_section.c:L66 _PyCriticalSection2_BeginSlow

func (*CriticalSection) End

func (c *CriticalSection) End(t *CSThread)

End pops the section. It panics if c is not on top of t's stack, which would indicate a Begin/End mismatch.

CPython: Python/critical_section.c:L173 PyCriticalSection_End

type Event

type Event struct {
	// contains filtered or unexported fields
}

Event mirrors PyEvent from cpython/Python/lock.c. It is a one-shot flag: once Notified, it stays set forever and every Wait returns immediately.

State machine on the byte-flag word:

0          unset, no waiters
flagLocked set
flagHasParked unset, with parked waiters

CPython: Include/internal/pycore_lock.h:L67 PyEvent

func (*Event) IsSet

func (e *Event) IsSet() bool

IsSet reports whether the event has been notified.

CPython: Python/lock.c:L256 _PyEvent_IsSet

func (*Event) Notify

func (e *Event) Notify()

Notify sets the event. If waiters are parked, all are woken. Repeat calls are safe and have no effect.

CPython: Python/lock.c:L263 _PyEvent_Notify

func (*Event) Wait

func (e *Event) Wait()

Wait blocks until the event is set.

CPython: Python/lock.c:L281 PyEvent_Wait

func (*Event) WaitTimed

func (e *Event) WaitTimed(timeout time.Duration, detach bool) bool

WaitTimed blocks until the event is set or timeout elapses. It returns true if the event is set, false on timeout.

timeout < 0 means block forever. detach is plumbed through for the PEP 703 protocol; it has no effect in v0.1.

CPython: Python/lock.c:L288 PyEvent_WaitTimed

type LockFlags

type LockFlags uint32

LockFlags bitset for LockTimed.

CPython: Include/internal/pycore_lock.h:L35 _PyLockFlags

const (
	// LockDetach asks the runtime to detach the current thread state
	// while parked. v0.1 has no thread state, so this is a no-op.
	LockDetach LockFlags = 1 << 0
	// LockHandleSignals asks the runtime to run pending signal
	// handlers if the wait was interrupted. v0.1 has no signal
	// plumbing, so this is a no-op.
	LockHandleSignals LockFlags = 1 << 1
)

type LockStatus

type LockStatus int

LockStatus mirrors PyLockStatus.

CPython: Include/pythread.h:L12 PyLockStatus

const (
	// LockFailure means the lock could not be acquired.
	LockFailure LockStatus = iota
	// LockAcquired means the lock is now held by the caller.
	LockAcquired
	// LockIntr means the wait was interrupted by a signal. v0.1 has
	// no signal plumbing, so this value is reserved.
	LockIntr
)

type Mutex

type Mutex struct {
	// contains filtered or unexported fields
}

Mutex is the byte-flag mutex from cpython/Python/lock.c.

The zero value is an unlocked mutex. Mutex must not be copied after first use, the same as Go's sync.Mutex.

CPython: Include/cpython/lock.h:L29 PyMutex

func (*Mutex) IsLocked

func (m *Mutex) IsLocked() bool

IsLocked reports whether the mutex is currently held.

CPython: Python/lock.c:L635 PyMutex_IsLocked

func (*Mutex) Lock

func (m *Mutex) Lock()

Lock blocks until the mutex is acquired.

CPython: Python/lock.c:L618 PyMutex_Lock

func (*Mutex) LockTimed

func (m *Mutex) LockTimed(timeout time.Duration, flags LockFlags) LockStatus

LockTimed acquires the mutex with a timeout. timeout < 0 means block forever; timeout == 0 is a non-blocking try.

CPython: Python/lock.c:L53 _PyMutex_LockTimed

func (*Mutex) TryLock

func (m *Mutex) TryLock() bool

TryLock acquires the mutex without blocking. It returns true on success.

CPython: Include/internal/pycore_lock.h:L21 PyMutex_LockFast (adapted from)

func (*Mutex) Unlock

func (m *Mutex) Unlock()

Unlock releases the mutex. It panics if the mutex is not locked, matching the Py_FatalError in PyMutex_Unlock.

CPython: Python/lock.c:L625 PyMutex_Unlock

type OnceFlag

type OnceFlag struct {
	// contains filtered or unexported fields
}

OnceFlag is a one-shot initialization flag. The zero value is ready to use.

CPython: Include/cpython/modsupport.h:L7 _PyOnceFlag

func (*OnceFlag) Do

func (o *OnceFlag) Do(fn OnceFn) error

Do invokes fn exactly once. Concurrent callers block until the runner finishes. If fn returns an error, the flag is reset and a later Do call can retry.

CPython: Python/lock.c:L335 _PyOnceFlag_CallOnceSlow

func (*OnceFlag) Done

func (o *OnceFlag) Done() bool

Done reports whether the flag has reached the initialized state.

CPython: Include/internal/pycore_lock.h:L140 _PyOnceFlag_CallOnce (adapted from)

type OnceFn

type OnceFn func() error

OnceFn is the function invoked by OnceFlag.Do. Returning a non-nil error resets the flag so a later caller can retry.

CPython: Include/internal/pycore_lock.h:L127 _Py_once_fn_t

type ParkStatus

type ParkStatus int

ParkStatus is the result of a Park call.

CPython: Include/internal/pycore_parking_lot.h:L22 Py_PARK_OK enum (adapted from)

const (
	// ParkOK means the thread was woken by Unpark or UnparkAll.
	ParkOK ParkStatus = iota
	// ParkTimeout means the timeout elapsed before a wakeup.
	ParkTimeout
	// ParkAgain means the address-equality check failed before parking.
	ParkAgain
	// ParkIntr is reserved for future signal handling. It is never
	// returned in v0.1 because gopy has no signal plumbing yet.
	ParkIntr
)

func Park

func Park(addr unsafe.Pointer, check func() bool, timeout time.Duration,
	parkArg any, detach bool,
) ParkStatus

Park atomically checks `check()` and, if it returns true, blocks the caller until Unpark or UnparkAll fires for the same addr, the timeout elapses, or the address-equality check fails before parking.

addr is the unique key. It is hashed by uintptr identity, not by the bytes it points at; the C code's `atomic_memcmp` step is the caller's responsibility, performed inside `check`.

If check returns false, Park returns ParkAgain without blocking.

timeout < 0 means "no timeout". timeout == 0 still parks (matches CPython: zero is special only inside Mutex.LockTimed, not at the parking-lot layer). Callers wanting a no-block try should not call Park.

detach is plumbed through for the PEP 703 attach/detach protocol; it has no effect in v0.1.

CPython: Python/parking_lot.c:L329 _PyParkingLot_Park

type RWMutex

type RWMutex struct {
	// contains filtered or unexported fields
}

RWMutex mirrors _PyRWMutex from cpython/Python/lock.c. It is a reader-writer lock with the same bit packing as the C code:

bit 0      writer holds the lock
bit 1      at least one waiter is parked
bits 2..   reader count

Writers are not starved: once a writer parks, new readers also park rather than slipping past.

The zero value is an unlocked RWMutex. Like the other pysync primitives, it must not be copied after first use.

CPython: Include/internal/pycore_lock.h:L187 _PyRWMutex

func (*RWMutex) Lock

func (r *RWMutex) Lock()

Lock acquires the write lock, parking until both readers and any other writer have released.

CPython: Python/lock.c:L508 _PyRWMutex_Lock

func (*RWMutex) RLock

func (r *RWMutex) RLock()

RLock acquires a read lock, parking until both the writer slot is free and no writer is waiting.

CPython: Python/lock.c:L464 _PyRWMutex_RLock

func (*RWMutex) RUnlock

func (r *RWMutex) RUnlock()

RUnlock releases a read lock. It panics if no read lock is held.

CPython: Python/lock.c:L495 _PyRWMutex_RUnlock

func (*RWMutex) Unlock

func (r *RWMutex) Unlock()

Unlock releases the write lock. It panics if the lock was not write-locked.

CPython: Python/lock.c:L529 _PyRWMutex_Unlock

type RawMutex

type RawMutex struct {
	// contains filtered or unexported fields
}

RawMutex mirrors _PyRawMutex from cpython/Python/lock.c. The C implementation packs the linked list of waiters into the mutex word itself, so it does not need the parking lot. CPython uses it only as the per-bucket lock inside the parking lot, where reusing the parking lot would create a bootstrap cycle.

In gopy the parking-lot bucket lock is Go's sync.Mutex, so the bootstrap cycle does not exist. This type is a thin wrapper around sync.Mutex provided for source-shape parity. The pointer-tagged uintptr trick that lock.c uses cannot be expressed without triggering go vet's unsafeptr check; if a future caller needs the exact CPython memory layout we will revisit.

CPython: Include/internal/pycore_lock.h:L97 _PyRawMutex

func (*RawMutex) Lock

func (r *RawMutex) Lock()

Lock acquires the mutex.

CPython: Python/lock.c:L191 _PyRawMutex_LockSlow

func (*RawMutex) Unlock

func (r *RawMutex) Unlock()

Unlock releases the mutex. It panics if the mutex was not locked, matching the Py_FatalError in _PyRawMutex_UnlockSlow.

CPython: Python/lock.c:L231 _PyRawMutex_UnlockSlow

type RecursiveMutex

type RecursiveMutex struct {
	// contains filtered or unexported fields
}

RecursiveMutex mirrors _PyRecursiveMutex from cpython/Python/lock.c. The same goroutine ident may relock; other goroutines block until every relock has been released.

The owner is identified by a pythread.Ident, so RecursiveMutex is only useful when callers have an explicit ident to pass. v0.1's usage is bounded; v0.7 (state) wires it up to the per-thread state.

CPython: Include/internal/pycore_lock.h:L149 _PyRecursiveMutex

func (*RecursiveMutex) IsLockedBy

func (r *RecursiveMutex) IsLockedBy(id pythread.Ident) bool

IsLockedBy reports whether the recursive mutex is currently held by the given ident.

CPython: Python/lock.c:L375 _PyRecursiveMutex_IsLockedByCurrentThread

func (*RecursiveMutex) Lock

func (r *RecursiveMutex) Lock(id pythread.Ident)

Lock acquires the mutex on behalf of id. If id already holds it, the recursion depth is incremented.

CPython: Python/lock.c:L381 _PyRecursiveMutex_Lock

func (*RecursiveMutex) Unlock

func (r *RecursiveMutex) Unlock(id pythread.Ident)

Unlock releases one level. It panics if id does not hold the mutex.

CPython: Python/lock.c:L410 _PyRecursiveMutex_Unlock

type SeqLock

type SeqLock struct {
	// contains filtered or unexported fields
}

SeqLock mirrors _PySeqLock from cpython/Python/lock.c. Writers take an exclusive lock that toggles the low bit on entry and exit; readers retry if the sequence number changed mid-read or is odd.

CPython: Include/internal/pycore_lock.h:L208 _PySeqLock

func (*SeqLock) AbandonWrite

func (s *SeqLock) AbandonWrite()

AbandonWrite rolls back a LockWrite without publishing changes.

CPython: Python/lock.c:L564 _PySeqLock_AbandonWrite

func (*SeqLock) AfterFork

func (s *SeqLock) AfterFork() bool

AfterFork resets the sequence if it was mid-update at fork time. Returns true if a reset happened.

CPython: Python/lock.c:L604 _PySeqLock_AfterFork

func (*SeqLock) BeginRead

func (s *SeqLock) BeginRead() uint32

BeginRead returns a sequence token to pass to EndRead. It spins until a non-updating sequence is observed.

CPython: Python/lock.c:L578 _PySeqLock_BeginRead

func (*SeqLock) EndRead

func (s *SeqLock) EndRead(previous uint32) bool

EndRead returns true if the sequence number is unchanged since the matching BeginRead. Callers retry the read on a false result.

CPython: Python/lock.c:L589 _PySeqLock_EndRead

func (*SeqLock) LockWrite

func (s *SeqLock) LockWrite()

LockWrite acquires write access by moving the sequence to an odd value. Concurrent writers spin until the lock is released.

CPython: Python/lock.c:L543 _PySeqLock_LockWrite

func (*SeqLock) UnlockWrite

func (s *SeqLock) UnlockWrite()

UnlockWrite publishes changes and releases the lock by moving the sequence to an even value.

CPython: Python/lock.c:L571 _PySeqLock_UnlockWrite

type UnparkFn

type UnparkFn func(parkArg any, hasMore bool)

UnparkFn is invoked under the bucket lock when Unpark finds a waiter (or with parkArg == nil if no waiter was queued). hasMore reports whether the bucket still has waiters on the same addr after the dequeue.

CPython: Include/internal/pycore_parking_lot.h _Py_unpark_fn_t (adapted from)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL