Help us shape the future of Go! Take the 2026 Go Developer Survey

nsync

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 7 Imported by: 17

README

nsync

Go Reference

Synchronization primitives for Go: timed and context-aware locks, named locks, semaphores, a bounded goroutine executor, and an atomic flag with an optional external lock.

Requires Go 1.27 or later. The development toolchain is Go 1.27.1. The package uses only the standard library. Synchronization uses atomic fast paths and condition variables, with no channels in the library implementation. The default backend is portable Go. On amd64, arm64, the other 64-bit architectures, and 32-bit MIPS, compiler intrinsics emit the atomic instructions inline; on 386, 32-bit ARM, and WebAssembly, sync/atomic operations are calls into the runtime's atomic routines.

go get github.com/vburenin/nsync

TryMutex

Create a mutex with NewTryMutex(). It provides Lock, Unlock, TryLock, TryLockTimeout(time.Duration), and LockContext(ctx). The try methods return whether the lock was acquired. Unlocking an unlocked mutex panics. Copies of a constructed TryMutex share the same lock.

Waiters queue in arrival order. A released mutex may be taken by a running goroutine before the woken waiter runs, which keeps contended throughput high; once the first waiter has waited about 100 µs, ownership is handed to waiters directly. This is not a strict FIFO or latency guarantee.

For ordinary locks without a timeout, the standard library's sync.Mutex also provides TryLock.

NamedMutex

NamedMutex acquires independent locks by string name. Its zero value is ready to use; NewNamedMutex() is also available.

It provides Lock(name), Unlock(name), TryLock(name), TryLockTimeout(name, timeout), and LockContext(ctx, name). Unlocking a name that is not locked panics.

Locks for names that no goroutine holds or waits for are discarded, apart from the bounded set of idle locks described next, so any number of distinct names can be used: memory follows the names currently in use, not every name ever locked. To keep repeated locking of hot names allocation-free, an instance retains at most 129 idle locks: one for the first name it locked, and up to one cached and one spare lock in each of its 64 shards.

Semaphore

NewSemaphore(capacity) limits concurrent acquisitions. Capacity must be positive; zero or negative capacities panic. On 32-bit platforms, capacities above 67,108,863 are reduced to it.

It provides Acquire, Release, TryAcquire, TryAcquireTimeout, AcquireContext(ctx), and Value. Value reports the number of occupied slots. Releasing without an acquisition panics. Do not copy a semaphore after first use.

All timed acquisition methods try immediately before waiting. Zero or negative timeouts perform a single nonblocking attempt.

OnceMutex and NamedOnceMutex

OnceMutex.Lock() returns true for the first acquisition. Other calls block until Unlock, then return false. Only the successful caller should unlock it. Its zero value is ready to use. LockContext(ctx) also stops waiting when the context is done.

NamedOnceMutex maintains an independent OnceMutex per key. Its zero value is ready to use. Keys must be comparable and equal to themselves (avoid NaN keys). Unlock(key) discards the completed mutex, so a later Lock(key) starts a new cycle. Unlocking an unknown key does nothing. This can combine overlapping cache refreshes for the same key into a single operation. LockContext(ctx, key) is the context-aware form of Lock.

ControlWaitGroup

NewControlWaitGroup(poolSize) limits the number of tasks running concurrently. The pool size must be positive. Do(func()) blocks until a slot is available, then launches the task and returns true. Wait() waits for running tasks and pending submissions to finish. Blocked submissions are admitted in arrival order.

workers := nsync.NewControlWaitGroup(4)
for _, job := range jobs {
    workers.Do(func() { process(job) })
}
workers.Wait()

Abort() permanently rejects new tasks and unblocks pending Do calls, which return false. Already admitted tasks may continue running; call Wait to wait for them. Repeated calls to Abort are safe.

DoContext(ctx, func()) and WaitContext(ctx) are the context-aware forms. DoContext returns ErrAborted once the group is aborted, unless its context is already done when it is called.

As with sync.WaitGroup, submission to an empty group must precede Wait. Before reusing a group, wait for all previous Wait calls to return. Working and Waiting are snapshots for monitoring, not synchronization.

Context-aware waiting

TryMutex.LockContext, NamedMutex.LockContext, Semaphore.AcquireContext, and ControlWaitGroup.DoContext return nil once they acquire. Otherwise they return ctx.Err() without acquiring, except that DoContext called with a live context returns ErrAborted once the group is aborted. As in golang.org/x/sync/semaphore, a context that is already done fails immediately, even if the lock is free. OnceMutex.LockContext and ControlWaitGroup.WaitContext still report an operation or group that has already completed.

The context is checked once before the first attempt and otherwise only while waiting. On an AMD Ryzen 9 5950X, context.Background() adds about 0.15 ns to an uncontended TryMutex.LockContext (a Lock/Unlock pair takes 3.6 ns without a context), under 0.1 ns to Semaphore.AcquireContext, and about 1.5 ns to NamedMutex.LockContext (6.2 ns per pair); a cancelable context adds 0.4–0.6 ns, or about 2.2 ns for NamedMutex. A canceled or timed-out waiter wakes alone and leaves the queue without disturbing other waiters.

SyncFlag

SyncFlag has an unset zero value. Set and Unset serialize writes with its embedded mutex; IsSet and IsUnset read atomically. Hold Lock to prevent changes, then call Unlock to allow changes again. Calling Set or Unset while holding that mutex would deadlock.

Development

go test -race -shuffle=on ./...
go vet ./...
./benchmarks/cross-build.sh
./benchmarks/compare-revisions.sh <git-revision> benchmarks/results/local

Stress tests mix blocking, nonblocking, timed, and context-aware acquisitions, with and without forced handoff, and check exclusion, capacity, and that every lock returns to its idle state. Use -short for a quicker run.

See performance measurements and design decisions for the design, reproducible benchmarks, rejected experiments, allocation measurements, and throughput/latency tradeoffs. The current code is measured in the round 3 report on an AMD Ryzen 9 5950X; earlier rounds include Apple M3 Max ARM64 results.

Documentation

Overview

Package nsync provides timed and context-aware locks, named locks, semaphores, a bounded goroutine executor, and an atomic flag with an external lock.

Acquisitions use atomic fast paths and park on condition variables, without channels. Waiters queue in arrival order; a released lock may still go to a running goroutine first, until a waiter has waited long enough to receive it directly. Timed and context-aware waits wake only the waiter that gives up.

NamedMutex keeps a lock for each string name while it is held or awaited, plus at most 129 idle locks for reuse, so its memory does not grow with the number of names ever locked. NamedOnceMutex combines overlapping operations for a comparable key and removes completed operations. Semaphore bounds concurrent acquisitions, while ControlWaitGroup bounds concurrently running functions and supports canceling pending submissions.

TryMutex, Semaphore, and ControlWaitGroup require their constructors. The zero values of NamedMutex, OnceMutex, NamedOnceMutex, and SyncFlag are usable.

Index

Constants

This section is empty.

Variables

View Source
var ErrAborted = errors.New("nsync: control wait group aborted")

ErrAborted is returned by DoContext when the group has been aborted.

Functions

This section is empty.

Types

type ControlWaitGroup

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

ControlWaitGroup runs tasks with a limit on concurrent goroutines. Use NewControlWaitGroup to initialize it. A ControlWaitGroup must not be copied after first use.

func NewControlWaitGroup

func NewControlWaitGroup(poolSize int) *ControlWaitGroup

NewControlWaitGroup creates a group. It panics unless poolSize is positive.

func (*ControlWaitGroup) Abort

func (cwg *ControlWaitGroup) Abort()

Abort unblocks pending Do calls and permanently rejects new tasks. Already admitted tasks may continue running; use Wait to wait for them. Repeated calls are safe.

func (*ControlWaitGroup) Do

func (cwg *ControlWaitGroup) Do(userFunc func()) bool

Do waits for a free slot and starts userFunc in a new goroutine, returning true. If the group is aborted before admission, Do returns false without running userFunc. Submission to an empty group must precede Wait.

func (*ControlWaitGroup) DoContext

func (cwg *ControlWaitGroup) DoContext(ctx context.Context, userFunc func()) error

DoContext is like Do, but stops waiting for a slot when ctx is done. It returns nil once userFunc has been started, ErrAborted if the group was aborted first, or ctx.Err(). If ctx is already done, DoContext does not start userFunc.

func (*ControlWaitGroup) Wait

func (cwg *ControlWaitGroup) Wait()

Wait waits for all admitted tasks and pending Do calls to finish. Submission to an empty group must precede Wait. Before reusing a group, all previous Wait calls must have returned.

func (*ControlWaitGroup) WaitContext

func (cwg *ControlWaitGroup) WaitContext(ctx context.Context) error

WaitContext is like Wait, but stops waiting when ctx is done. It returns nil once the group is idle, or ctx.Err() if ctx is done first.

func (*ControlWaitGroup) Waiting

func (cwg *ControlWaitGroup) Waiting() int

Waiting returns a snapshot of the number of pending Do calls.

func (*ControlWaitGroup) Working

func (cwg *ControlWaitGroup) Working() int

Working returns a snapshot of the number of occupied worker slots.

type NamedMutex

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

NamedMutex provides independent locks by name. The zero value is ready to use. A NamedMutex must not be copied after first use.

A name's lock exists while a goroutine holds it or waits for it; it is discarded afterwards, so memory does not grow with the number of names ever used. To keep repeated use of the same names allocation-free, an instance retains a bounded number of idle locks: one for the first name it locked, and at most one cached and one spare lock in each of its 64 shards.

func NewNamedMutex

func NewNamedMutex() *NamedMutex

NewNamedMutex creates a named mutex.

func (*NamedMutex) Lock

func (nm *NamedMutex) Lock(name string)

Lock acquires the named lock, creating it if necessary.

func (*NamedMutex) LockContext

func (nm *NamedMutex) LockContext(ctx context.Context, name string) error

LockContext acquires the named lock, blocking until it is available or ctx is done. It returns nil once the lock is acquired, or ctx.Err() without acquiring it. If ctx is already done, LockContext does not try to acquire.

func (*NamedMutex) TryLock

func (nm *NamedMutex) TryLock(name string) bool

TryLock tries to acquire the named lock without waiting.

func (*NamedMutex) TryLockTimeout

func (nm *NamedMutex) TryLockTimeout(name string, timeout time.Duration) bool

TryLockTimeout tries immediately, then waits up to timeout for the named lock. A non-positive timeout is equivalent to TryLock.

func (*NamedMutex) Unlock

func (nm *NamedMutex) Unlock(name string)

Unlock releases the named lock. It panics if the name is not locked.

type NamedOnceMutex

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

NamedOnceMutex combines overlapping operations for the same comparable key. One caller's Lock returns true; concurrent callers wait for its Unlock and return false. After Unlock, a new call may start another operation for the key. The zero value is ready to use and must not be copied after first use. Keys must be comparable and equal to themselves (NaN keys are unsupported).

func NewNamedOnceMutex

func NewNamedOnceMutex() *NamedOnceMutex

NewNamedOnceMutex creates a named once mutex.

func (*NamedOnceMutex) Lock

func (nom *NamedOnceMutex) Lock(key any) bool

Lock starts an operation for key, or waits for the current operation to finish. Only callers receiving true should call Unlock.

func (*NamedOnceMutex) LockContext

func (nom *NamedOnceMutex) LockContext(ctx context.Context, key any) (bool, error)

LockContext is like Lock, but stops waiting when ctx is done. It returns (true, nil) if the caller must perform the operation and then call Unlock, (false, nil) once the operation in progress has completed, or (false, ctx.Err()) if ctx is done first. A done ctx never starts an operation.

func (*NamedOnceMutex) Unlock

func (nom *NamedOnceMutex) Unlock(key any)

Unlock completes the active operation for key. Unknown keys are ignored. Only unshared entries can be recycled; a shared entry remains alive through the references held by its waiters, independently of later operations.

type OnceMutex

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

OnceMutex admits one operation. The first Lock returns true; other calls wait for Unlock and return false. Its zero value is ready to use. It must not be copied after first use.

func NewOnceMutex

func NewOnceMutex() *OnceMutex

NewOnceMutex creates a once mutex.

func (*OnceMutex) Lock

func (om *OnceMutex) Lock() bool

Lock starts the operation or waits for the successful caller's Unlock.

func (*OnceMutex) LockContext

func (om *OnceMutex) LockContext(ctx context.Context) (bool, error)

LockContext is like Lock, but stops waiting when ctx is done. It returns (true, nil) if the caller must perform the operation and then call Unlock, (false, nil) once another caller's operation has completed, or (false, ctx.Err()) if ctx is done first. A done ctx never starts the operation, but an already completed operation is reported as such.

func (*OnceMutex) Unlock

func (om *OnceMutex) Unlock()

Unlock completes the operation and publishes its writes to waiting callers. It panics unless Lock has succeeded and Unlock has not been called before.

type Semaphore

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

Semaphore limits concurrent acquisitions. Use NewSemaphore to initialize it. A Semaphore must not be copied after first use.

func NewSemaphore

func NewSemaphore(value int) *Semaphore

NewSemaphore creates a semaphore. It panics unless value is positive. Capacities above 2^58-1, or 2^26-1 (67,108,863) on 32-bit platforms, are reduced to that maximum.

func (*Semaphore) Acquire

func (s *Semaphore) Acquire()

Acquire acquires a slot, blocking when all slots are occupied.

func (*Semaphore) AcquireContext

func (s *Semaphore) AcquireContext(ctx context.Context) error

AcquireContext acquires a slot, blocking until one is available or ctx is done. It returns nil once a slot is acquired, or ctx.Err() without acquiring one. If ctx is already done, AcquireContext does not try to acquire.

func (*Semaphore) Release

func (s *Semaphore) Release()

Release releases a slot. It panics if no slot is occupied.

func (*Semaphore) TryAcquire

func (s *Semaphore) TryAcquire() bool

TryAcquire acquires an available slot without waiting.

func (*Semaphore) TryAcquireTimeout

func (s *Semaphore) TryAcquireTimeout(d time.Duration) bool

TryAcquireTimeout tries immediately, then waits up to d for a slot. A non-positive duration is equivalent to TryAcquire.

func (*Semaphore) Value

func (s *Semaphore) Value() int

Value returns a snapshot of the number of occupied slots.

type SyncFlag

type SyncFlag struct {
	sync.Mutex
	// contains filtered or unexported fields
}

SyncFlag implements a boolean flag that can be set or unset atomically. During set/unset SyncFlag locks the mutex, so if anything needs to prevent a flag from being set/unset should acquire a lock. The zero value is unset. A SyncFlag must not be copied after first use. Set and Unset must not be called while the caller holds the embedded mutex.

func (*SyncFlag) IsSet

func (bf *SyncFlag) IsSet() bool

IsSet atomically checks if flag is set.

func (*SyncFlag) IsUnset

func (bf *SyncFlag) IsUnset() bool

IsUnset atomically checks if flag is unset.

func (*SyncFlag) Set

func (bf *SyncFlag) Set()

Set locks the mutex, sets the flag and unlocks the mutex.

func (*SyncFlag) Unset

func (bf *SyncFlag) Unset()

Unset locks the mutex, resets the flag and unlocks the mutex.

type TryMutex

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

TryMutex provides blocking, nonblocking, timed, and context-aware lock acquisition. Use NewTryMutex to initialize it. Copies refer to the same underlying lock.

func NewTryMutex

func NewTryMutex() *TryMutex

NewTryMutex creates an unlocked mutex.

func (TryMutex) Lock

func (tm TryMutex) Lock()

Lock acquires the mutex, blocking if it is already locked.

func (TryMutex) LockContext

func (tm TryMutex) LockContext(ctx context.Context) error

LockContext acquires the mutex, blocking until it is available or ctx is done. It returns nil once the mutex is acquired, or ctx.Err() without acquiring it. If ctx is already done, LockContext does not try to acquire.

func (TryMutex) TryLock

func (tm TryMutex) TryLock() bool

TryLock acquires the mutex without waiting, returning true on success.

func (TryMutex) TryLockTimeout

func (tm TryMutex) TryLockTimeout(timeout time.Duration) bool

TryLockTimeout tries immediately, then waits up to timeout for the mutex. A non-positive timeout is equivalent to TryLock.

func (TryMutex) Unlock

func (tm TryMutex) Unlock()

Unlock releases the mutex. It panics if the mutex is unlocked.

Directories

Path Synopsis
benchmarks
asm
Package asmbench compares handwritten assembly with compiler intrinsics.
Package asmbench compares handwritten assembly with compiler intrinsics.
internal
baseline
Frozen pre-optimization implementation.
Frozen pre-optimization implementation.

Jump to

Keyboard shortcuts

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