README
¶
darvaza.org/x/sync
Package sync provides interfaces and utilities for synchronisation
primitives.
Overview
This package defines standardised interfaces for synchronisation primitives that complement the standard library while providing additional functionality.
The primitives handle panic situations that may arise from underlying mutexes. When panics occur (typically indicating development mistakes rather than runtime errors), they are aggregated while enabling proper clean-up of other locks. This approach ensures resources are properly released rather than leaked, even during panic scenarios.
Features
- Standardised
MutexandRWMutexinterfaces. - Context-aware mutex interfaces for cancellation and timeout support.
- Functions for operating on multiple locks simultaneously.
- Safe lock/unlock operations with proper error handling.
- Lightweight spinlock implementation for low-contention scenarios.
- Semaphore implementation supporting both exclusive and shared access patterns.
- Condition-based synchronisation primitives for goroutine coordination.
- Atomic helpers shadowing
sync/atomicfor bit-mask OR and max-value updates.
Package Structure
darvaza.org/x/sync: The main package namespace.atomic: Shadows the standard librarysync/atomicpackage and adds helpers for bit-mask OR and monotonic max updates.cond: Contains condition-based synchronisation primitives for coordinating goroutines.errors: Contains error types and helpers for implementing common synchronisation primitives.mutex: Contains interfaces and utilities for mutex operations.semaphore: Provides a cancellable read-write mutex implementation with counting semaphore algorithms.spinlock: Contains a lightweight spinlock implementation ofmutex.Mutex.workgroup: Provides concurrent task management and synchronisation within a shared lifecycle.
Integration with the Package
The cond package complements the other synchronisation primitives in the
darvaza.org/x/sync package:
- Layered architecture:
Barrierprovides building blocks that other synchronisation mechanisms likesemaphorecan use internally. - Consistent interfaces: Follows similar patterns to other package components for error handling and method naming.
- Complementary functionality: Addresses coordination use cases that mutexes and spinlocks don't directly solve.
- Composability: Can be used alongside other primitives to create advanced synchronisation patterns.
The addition of the cond package makes the darvaza.org/x/sync package
more complete by addressing a broader range of concurrency control
scenarios beyond just mutual exclusion, enabling more sophisticated
coordination between concurrent operations.
This completes the synchronisation primitive ecosystem with:
- Mutex interfaces and utilities for exclusion (
mutex). - Lightweight spinlocks for low-contention cases (
spinlock). - Counting semaphores for resource control (
semaphore). - Coordination barriers for signalling and waiting (
cond).
Interfaces
Mutex
The core interface for mutual exclusion operations:
type Mutex interface {
// Lock acquires the mutex, blocking until it is available.
Lock()
// TryLock attempts to acquire the mutex without blocking.
// Returns true if the lock was acquired, false otherwise.
TryLock() bool
// Unlock releases the mutex.
// Calling Unlock on an unlocked mutex is expected to panic.
Unlock()
}
The standard library's sync.Mutex{} and sync.RWMutex{} implement this
interface.
RWMutex
Extends the Mutex interface with read-locking capabilities:
type RWMutex interface {
Mutex
// RLock acquires a read lock on the mutex, blocking until available.
RLock()
// TryRLock attempts to acquire a read lock without blocking.
// Returns true if the lock was acquired, false otherwise.
TryRLock() bool
// RUnlock releases a read lock on the mutex.
// Calling RUnlock without holding a read lock is expected to panic.
RUnlock()
}
The standard library's sync.RWMutex{} implements this interface.
Context-Aware Interfaces
The package provides context-aware interfaces extending the basic mutex interfaces:
type MutexContext interface {
Mutex
// LockContext acquires the mutex with context awareness.
// It blocks until the lock is acquired or the context is done.
// Returns an error if the context is cancelled or times out.
LockContext(context.Context) error
}
type RWMutexContext interface {
RWMutex
MutexContext
// RLockContext acquires a read lock with context awareness.
// It blocks until the read lock is acquired or the context is done.
// Returns an error if the context is cancelled or times out.
RLockContext(context.Context) error
}
These interfaces serve primarily as extension points for implementers. Although standard library mutex types don't implement them directly, the package provides helper functions to work with these interfaces. Custom mutex implementations can adopt these interfaces to provide context-aware locking capabilities that respect cancellation and timeouts.
Barrier
The cond package provides a Barrier type that implements a coordination
mechanism for goroutines using a token-based approach.
type Barrier struct{}
A Barrier manages a reusable token that can be used to signal state changes
and coordinate access to shared resources. It's primarily designed to be used
by other synchronisation primitives internally.
Each Barrier instance needs to be initialised before use and closed when
no longer needed to release resources.
Barrier Characteristics
- Thread safety: Provides safe coordination between multiple goroutines
- Reusable signalling: Supports broadcasting to all waiters or signalling individual goroutines
- Non-blocking options: Includes both blocking and non-blocking APIs
- Resource efficiency: Uses a channel-based token mechanism with minimal memory overhead
Core methods
Broadcast(): Notifies all waiting goroutines at once.Signal() bool: Attempts to wake up a single waiting goroutine.Wait(): Blocks until the barrier is signalled.Signaled() <-chan struct{}: Returns a channel for select-based waiting.Acquire()/Release(Token): Manual token acquisition and release for fine-grained control.
Token concept
The Token type is a channel-based synchronisation primitive that allows
goroutines to wait for and signal state changes:
type Token chan struct{}
Tokens can be acquired from a barrier, waited upon, signalled individually, or closed to wake up all waiters simultaneously.
Barrier Implementation
- Uses a buffered channel with capacity 1 to store the token.
- Maintains an internal state to track if the barrier has been closed.
- Provides graceful handling of nil receivers and improper initialisation.
- Returns appropriate errors from the
errorspackage for common failure modes.
Count
The cond package also provides a Count type that combines features of a
condition variable and an atomic counter.
type Count struct{}
A Count allows atomic operations and waiting on an int32 value until specific
conditions are met. This enables goroutines to coordinate based on a numeric
value and user-defined conditions.
Each Count instance needs to be initialised and closed properly to avoid
resource leaks.
Count Characteristics
- Atomic counter operations: Provides thread-safe increment, decrement, and add operations.
- Conditional waiting: Supports waiting until the counter reaches specific values or meets custom conditions.
- Context-aware waiting: Includes methods that respect context cancellation and timeouts.
- Broadcast capability: Can notify all waiting goroutines when conditions are met.
Count core methods
Add(n int) int: Atomically adds n to the counter and returns the new value.Inc() int: Atomically increments the counter by 1.Dec() int: Atomically decrements the counter by 1.Value() int: Returns the current counter value.Wait(): Blocks until the counter becomes zero.WaitFn(func(int32) bool): Blocks until the provided condition function returns true.WaitFnContext(context.Context, func(int32) bool): Context-aware waiting with cancellation support.Reset(n int): Resets the counter to the specified value and wakes all waiting goroutines.Signal() bool: Wakes a single waiting goroutine.Broadcast(): Wakes all waiting goroutines.
Count Implementation
- Uses atomic operations for counter management to ensure thread safety.
- Leverages the
Barriertype internally for goroutine coordination. - Supports custom conditions for signalling waiters when specific values are reached.
- Provides robust error handling for nil receivers and uninitialised instances.
- Integrates with Go's context package for cancellation and timeout support.
CountZero
The cond package provides a specialised variant of Count called
CountZero that broadcasts specifically when the counter reaches zero.
type CountZero Count
This specialisation simplifies coordination in cases where completion is signified by a zero value, which is a common pattern in many concurrent applications.
CountZero Characteristics
- Zero-focused: Automatically wakes all waiters when the counter reaches zero.
- Simplified API: Built specifically for the zero-value condition.
- Identical core operations: Provides the same atomic counter
operations as
Count. - Streamlined waiting: Methods specifically wait for the zero condition.
Relation to Count
The CountZero type is implemented as a thin wrapper around Count with a
predefined condition function that checks for zero. It provides equivalent
functionality with these key differences:
- No need to specify custom condition functions.
- All waiting methods specifically wait for the counter to reach zero.
- Method signatures are simplified to focus on the zero-value use case.
Core Methods
CountZero provides the same atomic counter operations as Count (Add,
Inc, Dec, Value) and similar coordination methods with simpler
signatures:
Wait(): Blocks until the counter becomes zero.WaitAbort(<-chan struct{}): Blocks until zero or abort channel closes.WaitContext(context.Context): Blocks until zero or context cancellation.
Common Use Cases
The CountZero type excels in scenarios such as:
- Tracking completion of a known number of concurrent operations.
- Managing graceful shutdown processes.
- Implementing simple worker pools with completion signalling.
- Coordinating resource clean-up when all references are released.
Example Usage
// Create a counter to track 5 operations
counter := cond.NewCountZero(5)
defer counter.Close()
for i := 0; i < 5; i++ {
go func() {
// Perform work
// Decrement counter when done
counter.Dec()
}()
}
// Wait for all operations to complete.
if err := counter.Wait(); err != nil {
// Handle error.
}
// All operations complete when counter reaches zero.
CountZero provides a concise way to express the common pattern of
waiting for a counter to reach zero without the need for custom condition
functions.
Semaphore
The semaphore package provides a Semaphore type that implements both
exclusive and shared access patterns using a counting semaphore algorithm.
type Semaphore struct{}
The Semaphore fully implements the context-aware mutex interfaces:
sync.Lockermutex.Mutexmutex.MutexContext- supporting context cancellation and timeouts.mutex.RWMutexmutex.RWMutexContext- supporting context cancellation and timeouts.
This makes it compatible with all lock operations provided by the package, with comprehensive capabilities for both exclusive and shared access patterns.
Exclusive Locking Methods
Lock(): Acquires exclusive lock, panics on error.LockContext(ctx context.Context) error: Acquires exclusive lock with context cancellation support.TryLock() bool: Attempts non-blocking acquisition of exclusive lock.TryLockContext(ctx context.Context) (bool, error): Non-blocking attempt with context support.Unlock(): Releases exclusive lock, panics on error.
Shared Locking Methods
RLock(): Acquires a read lock, panics on error.RLockContext(ctx context.Context) error: Acquires read lock with context cancellation support.TryRLock() bool: Attempts non-blocking acquisition of read lock.TryRLockContext(ctx context.Context) (bool, error): Non-blocking attempt with context support.RUnlock(): Releases a read lock, panics on error.
The semaphore provides advanced synchronisation combining features of both mutexes and traditional semaphores, with integrated context-awareness for cancellation and timeout handling.
SpinLock
The spinlock package provides a lightweight spinlock implementation for
scenarios where locks are held for very brief periods.
type SpinLock uint32
SpinLock is a mutual exclusion primitive that uses active spinning
(busy-waiting) instead of parking goroutines. It implements both the
sync.Locker and mutex.Mutex interfaces.
SpinLock Characteristics
- Zero value: An unlocked spinlock ready for use.
- Memory footprint: Minimal (just a uint32).
- CPU usage: Consumes CPU cycles while waiting (unlike traditional mutexes).
- Target use cases: Low-contention scenarios with briefly held locks.
Methods
Lock(): Acquires the lock, spinning until successful.TryLock() bool: Attempts to acquire the lock without blocking.Unlock(): Releases the lock.
When to use
- Use when lock contention is rare and locks are held for minimal duration.
- Avoid when locks might be held for extended periods.
- Best for performance-critical code paths where context switching would be costly.
Example usage
var lock spinlock.SpinLock
// In concurrent code:
lock.Lock()
defer lock.Unlock()
// Critical section here (keep it very brief)
SpinLock Implementation
- Uses atomic operations for lock state management.
- Calls
runtime.Gosched()while spinning to yield the processor. - Panics with appropriate errors for nil receivers or unlocking unlocked spinlocks.
- Provides internal methods that return errors rather than panicking for composability.
Performance characteristics
Benchmark testing shows SpinLock is efficient for operations that complete quickly, as it avoids the overhead of parking and unparking goroutines or using channels.
Workgroup
The workgroup package provides concurrent task management and synchronisation
for coordinating multiple operations within a shared lifecycle.
type Group struct{}
Unlike sync.WaitGroup, the workgroup.Group integrates with contexts for
cancellation propagation and lifecycle management of concurrent operations.
Key features
- Context integration for propagating cancellation signals.
- Coordinated lifecycle management for concurrent tasks.
- Graceful shutdown via
Close: cancels the Group and drains all tasks. - Error tracking and propagation.
- Concurrent safety for multi-goroutine use.
Group Methods
Context() context.Context: Returns the context associated with the Group.Err() error: Returns the cancellation cause, if any.IsCancelled() bool: Reports whether the Group has been cancelled.Cancelled() <-chan struct{}: Returns a channel closed on cancellation.Done() <-chan struct{}: Returns a channel closed when all tasks complete.Wait() error: Blocks until all tasks complete.Cancel(error) bool: Cancels the Group with an optional error cause.Close() error: Cancels the Group and waits for all tasks to complete.Go(func(context.Context)) error: Spawns a new goroutine with context.GoCatch(func(context.Context) error, func(context.Context, error) error) error: Spawns a goroutine with error handling and error-triggered cancellation.GoShutdown(func(context.Context) error, func(context.Context), time.Duration) error: Spawns a supervised task paired with a shutdown handler that signals it to end when the Group is cancelled while it is still running — thehttp.ServerListenAndServe/Shutdownpattern for workers that cannot act on the Group's context.
Group Example usage
wg := workgroup.New(ctx)
defer wg.Close()
// Add tasks to the workgroup
wg.Go(func(ctx context.Context) {
// Task implementation with context-based cancellation
select {
case <-ctx.Done():
return // respond to cancellation
case <-time.After(1 * time.Second):
// do work
}
})
// Wait for all tasks to complete or context to be cancelled
if err := wg.Wait(); err != nil {
// Handle error.
}
Group Implementation details
- Propagates cancellation signals from parent contexts to all tasks.
- Provides an
OnCancelhook that fires on cancellation from any source — an explicitCancel/Closeor the parent context. The handler receives the cancellation cause and a live context detached from the Group's cancellation (viacontext.WithoutCancel): it retains the Group context's values but is not itself cancelled, so cleanup work can pass it along or build its own deadline on top. GoShutdownpairs a task with a shutdown handler in a single tracked unit. The handler's one job is to tell a worker that cannot act on the Group's context to end: it runs at most once, only when the Group is cancelled while the task is still running, and receives a live, detached context bounded by the grace period (unbounded when the grace period is zero or less). A task that ends on its own needs no signal — an error still cancels the Group, but the handler is not invoked. With a nil task the handler is enrolled alone as a watcher: the per-resource counterpart to the per-GroupOnCancelhook — one enrolled for each resource to release on cancellation, grace-bounded, without the cause. Like theOnCancelhandler, a panic in the shutdown handler is recovered and discarded.- Safe for concurrent use from multiple goroutines.
- Supports reuse after completion if not cancelled.
- Error tracking distinguishes between normal cancellation and error causes.
Atomic
The atomic package shadows the standard library sync/atomic package and
adds helpers for patterns recurring in synchronisation primitives.
The standard-library atomic types (Bool, Int32, Int64, Uint32,
Uint64, Uintptr, Value, Pointer[T]) are re-exported as aliases so
callers using the type-based API can reach both the stdlib types and the
extension helpers through a single import of darvaza.org/x/sync/atomic.
The legacy free-function API (AddInt32, LoadInt32, ...) is intentionally
omitted; new code should use the type-based methods that supersede it since
Go 1.19. Callers that still need those functions should keep their
sync/atomic import alongside this one.
Atomic Helpers
BitmaskOr(p *Uint32, mask uint32) (uint32, bool): Atomically applies a bitwise OR ofmaskto*p, returning the resulting value and whether any bits actually changed. The boolean signals first-writer semantics for the supplied mask; callers wanting a one-shot "target reached" notification gate onchanged && result == target.UpdateMax(p *Int32, val int32) int32: Atomically raises*ptovalwhenvalis greater than the current value; no-op otherwise. Returns the value the caller raised*pto, or the value observed when no update was performed.*pis guaranteed to be at least the returned value after the call, but may already be higher under contention.
Atomic Example usage
First-writer notification per bit:
import "darvaza.org/x/sync/atomic"
var bits atomic.Uint32
if _, changed := atomic.BitmaskOr(&bits, 0x01); changed {
// first writer to set this bit
}
One-shot "target reached" notification when the last expected bit lands:
import "darvaza.org/x/sync/atomic"
var bits atomic.Uint32
const target = uint32(0b111) // all three bits expected
myBit := uint32(0b010) // the bit this caller is responsible for
result, changed := atomic.BitmaskOr(&bits, myBit)
if changed && result == target {
// exactly one writer reaches here: the one whose OR completed target
}
Utility Functions
The package provides functions that make mutex operations safer by handling edge cases and panic conditions:
Single Mutex Operations
mutex.SafeLock[T sync.Locker](mu T) (bool, error): Safely acquires an exclusive lock, handling nil mutexes and panics.mutex.SafeRLock[T sync.Locker](mu T) (bool, error): Safely acquires a read lock (or normal lock if not RWMutex).mutex.SafeTryLock[T mutex.Mutex](mu T) (bool, error): Non-blocking attempt to acquire a lock with nil and panic handling.mutex.SafeTryRLock[T mutex.Mutex](mu T) (bool, error): Non-blocking attempt to acquire a read lock safely.mutex.SafeUnlock[T sync.Locker](mu T) error: Safely releases a lock, handling nil mutexes and panics.mutex.SafeRUnlock[T sync.Locker](mu T) error: Safely releases a read lock (or normal lock if not RWMutex).
Multiple Mutex Operations
mutex.Lock[T mutex.Mutex](locks ...T): Acquires multiple locks in order.mutex.TryLock[T mutex.Mutex](locks ...T) bool: Non-blocking attempt to acquire multiple locks.mutex.RLock[T mutex.Mutex](locks ...T): Acquires multiple read locks when possible.mutex.TryRLock[T mutex.Mutex](locks ...T) bool: Non-blocking attempt to acquire multiple read locks.mutex.Unlock(locks ...mutex.Mutex): Releases multiple locks.mutex.RUnlock(locks ...mutex.Mutex): Releases multiple read locks safely.mutex.ReverseUnlock[T Mutex](unlock func(T) error, locks ...T) error: Releases locks in reverse order, collecting any panics.
Context-Aware Operations
NewSafeLockContext[T MutexContext](ctx context.Context) func(mu T) (bool, error): Creates a function for context-aware locking.NewSafeRLockContext[T MutexContext](ctx context.Context) func(mu T) (bool, error): Creates a function for context-aware read locking.SafeLockContext[T MutexContext](ctx context.Context, mu T) (bool, error): Acquires a lock with context cancellation/timeout support.SafeRLockContext[T MutexContext](ctx context.Context, mu T) (bool, error): Acquires a read lock with context cancellation/timeout support.
These utility functions provide:
- Error handling: Returns explicit success/failure indicators.
- Type safety: Uses generics to work with any compatible type.
- Panic protection: Catches and converts panics into regular errors.
- Nil handling: Checks for nil mutexes, returning appropriate errors.
- Interface detection: Automatically uses RWMutex implementations when available.
Error Handling
The errors sub-package defines error types for common
synchronisation issues:
ErrAlreadyInitialised: Returned when a primitive that was already initialised is being initialised again.ErrNotInitialised: Returned when a primitive was expected to be initialised but was not.ErrClosed: Returned when operations cannot proceed because the target is closed.ErrNilContext: Returned when a nil context is encountered in context-aware operations.ErrNilMutex: Returned when a Mutex was expected but none was provided.ErrNilReceiver: Returned when methods are called on a nil receiver.
The package provides CompoundError, a concurrency-safe counterpart of
core.CompoundError that accumulates errors reported from several goroutines
and presents them as a single error. Errors and Unwrap return snapshot
copies so callers can iterate without holding the lock, while AsError
returns the receiver itself; the zero value is ready to use.
Internally the package uses core.CompoundError to collect and combine
multiple errors that may occur during operations on multiple mutexes. This
allows for unlocking all mutexes even when some operations fail, while still
providing comprehensive error reporting.
core.Catch() and core.PanicError convert panics into regular errors with
stack traces.
This package follows specific patterns for handling error conditions:
Panic Propagation
- Operations panic when encountering nil mutexes or when underlying mutex operations panic.
- Unlocking an unlocked mutex will panic as per standard Go mutex behaviour.
- When operating on multiple locks, panics from individual mutex operations are aggregated.
- During failures, the package ensures proper clean-up by releasing any successfully acquired locks before propagating the panic.
- Unlock operations attempt to unlock all provided mutexes even if some fail, collecting and aggregating errors during the process.
Design Philosophy
Panic conditions indicate programming mistakes rather than runtime conditions that should be handled differently. This approach was chosen because:
- Mutex misuse (e.g., unlocking an unlocked mutex) represents a development error.
- Aggregating panics allows for proper clean-up while still communicating the underlying issue.
- The interfaces lack error returns for lock/unlock operations, as these should not fail under normal circumstances.
- Unlock operations always attempt to release all locks, even if some fail, to prevent resource leaks.
For context-aware operations (using MutexContext and RWMutexContext
interfaces), explicit error handling is provided to manage cancellation and
timeout scenarios, which are considered valid runtime conditions rather than
programming errors.
Dependencies
This package only depends on the standard library and
darvaza.org/core.
Licence
This project is licensed under the MIT Licence. See LICENCE.txt for details.
Installation
go get darvaza.org/x/sync
Development
For development guidelines, architecture notes, and AI agent instructions, see AGENTS.md.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package atomic shadows the standard library sync/atomic package and adds helpers for patterns recurring in synchronisation primitives.
|
Package atomic shadows the standard library sync/atomic package and adds helpers for patterns recurring in synchronisation primitives. |
|
Package cond provides condition-based synchronisation primitives on top of the sync/atomic package and channels.
|
Package cond provides condition-based synchronisation primitives on top of the sync/atomic package and channels. |
|
Package errors provides synchronisation-related error definitions.
|
Package errors provides synchronisation-related error definitions. |
|
internal
|
|
|
synctesting
Package synctesting provides timing-aware assertion helpers for tests of channel- and synchronisation-primitive-driven code in darvaza.org/x/sync.
|
Package synctesting provides timing-aware assertion helpers for tests of channel- and synchronisation-primitive-driven code in darvaza.org/x/sync. |
|
Package mutex provides interfaces and utilities for mutual exclusion and synchronisation primitives.
|
Package mutex provides interfaces and utilities for mutual exclusion and synchronisation primitives. |
|
Package semaphore provides synchronisation primitives for controlling access to shared resources.
|
Package semaphore provides synchronisation primitives for controlling access to shared resources. |
|
Package spinlock provides a lightweight, efficient spinlock implementation for low-contention mutual exclusion scenarios.
|
Package spinlock provides a lightweight, efficient spinlock implementation for low-contention mutual exclusion scenarios. |
|
Package workgroup provides concurrent task management and synchronisation for coordinating multiple operations within a shared lifecycle.
|
Package workgroup provides concurrent task management and synchronisation for coordinating multiple operations within a shared lifecycle. |