Concurrency memory model¶
This page describes how shared memory is ordered when Gossamer goroutines communicate.
Gossamer has automatic memory management. Heap values stay alive while reachable, and compiled tiers promote reference counts to atomic counting when a value crosses a goroutine boundary. That protects object lifetime. It does not make ordinary reads and writes into a synchronization protocol.
Ordinary memory operations inside one goroutine run in program order. Between goroutines, an ordinary read is ordered after an ordinary write only when a synchronization operation connects them. Without such an edge, the operations may be observed in either order, and if both touch the same memory location with at least one write, the program has a data race. Gossamer keeps the managed object alive, but the racing value itself has no useful cross-goroutine ordering guarantee.
What this is like¶
The concurrency side is Go-shaped: goroutines share memory, and channels or synchronization primitives establish ordering. Sharing a pointer is not publication by itself.
The lifetime side is Swift-like: compiled code uses deterministic reference counting rather than a tracing collector. Unlike Swift ARC, compiled Gossamer also collects strong cycles, and it can keep goroutine-local reference counts non-atomic until an object is shared.
This is not Rust's model. & and &mut express access intent in the
type system, not a lifetime proof, and there is no borrow checker
surface or lifetime annotation language.
This is not C or C++ either. A data race is not documented as permission for the compiler to assume arbitrary facts about the whole program. It means the racing accesses are unordered by the Gossamer model, so the value observed there is not a reliable communication result. Use channels, locks, wait groups, barriers, once initialization, or atomics when another goroutine must observe a write.
Happens-before edges¶
A happens-before edge is the ordering relation used by the model. If A happens-before B, then effects before A are visible to code after B according to the synchronization primitive's contract.
Gossamer establishes these edges:
- Starting a goroutine publishes the values captured or passed to that goroutine before its body begins.
- A channel send publishes to the receive that obtains that value.
- Closing a channel publishes to receives that observe the closed and drained state.
selectinherits the send, receive, and close edge of the arm that actually runs. Adefaultarm does not synchronize with another goroutine.Mutexunlock publishes to a later lock of the same mutex.RwLockwrite unlock publishes to later readers and writers that acquire the same lock. Concurrent readers are mutually compatible but do not publish to each other merely by overlapping.WaitGroup::donepublishes to awaitcall that observes the count reach zero.Barrier::waitis a rendezvous for its participants. Code after a released barrier round runs after every participant reached that same round.Once::call_oncepublishes the completed initialization body to every caller that returns from the sameOnce.- Atomic operations follow their documented ordering. The default
load,store,fetch_add, and compare-and-swap APIs are sequentially consistent. Ordered APIs exposeRelaxed,Acquire,Release,AcqRel, andSeqCst; relaxed operations are atomic but do not publish non-atomic memory by themselves.
These edges compose transitively. For example, if goroutine A writes a value, sends on a channel, goroutine B receives that send and then unlocks a mutex, goroutine C's later lock of the same mutex is ordered after A's write.
Channels¶
channel() and channel(0) create an unbuffered rendezvous channel. A
send on an unbuffered channel does not complete until a receiver accepts
that exact value, and the send publishes to that receive.
channel(n) for n > 0 creates a bounded buffered channel. Sends block
when the buffer is full. Receives block when the buffer is empty. The
send of a buffered element publishes to the receive that removes that
element from the buffer.
channel::unbounded() creates an explicit unbounded queue channel for
code that intentionally wants producer sends to complete without a
receiver or capacity limit. Ordering is still per element: a send
publishes to the receive of that element.
Receiving from a closed and drained channel returns None. Closing an
already closed channel is an error.
Select¶
When multiple non-default select arms are ready, Gossamer polls arms in
pseudo-random order so source order does not deterministically win.
default is selected only when no send or receive arm is ready.
Blocking select registers interest in all channel arms and wakes when
any of those channels may have become ready. The selected arm determines
the synchronization edge. Arms that were registered but not selected do
not synchronize.
Atomics¶
Atomic values provide indivisible operations on their own storage. They are the right tool for counters, flags, and carefully designed lock-free handoffs.
Sequentially consistent atomics participate in a single global order for that atomic API surface. Release stores publish to acquire loads that observe the released value. AcqRel read-modify-write operations both acquire from the value they read and release the value they write.
Relaxed atomic operations are still atomic: there is no torn read or write of the atomic value. They do not order ordinary memory. A relaxed counter can count events, but it does not publish the data associated with those events unless some other synchronization does that work.
Race detector¶
gos test --race and race-enabled compiled code instrument heap
accesses and report conflicting non-atomic accesses that are not ordered
by detector-visible synchronization. The detector uses vector clocks and
records handoff edges for goroutine scheduling, channels, mutexes, wait
groups, once initialization, and detector-aware atomic operations.
The detector is diagnostic tooling, not the definition of the language. It can miss synchronization it does not currently model, such as some reader-writer-lock paths, and it does not make an unordered program ordered. Treat a reported race as a real bug unless there is a precise reason that the detector lacks the relevant edge.