Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Shared owners

Shared[T] is one heap value with several owners. Each owner is an affine handle; the value is released when the last handle drops, on whichever thread drops it. Use it to give pool jobs, or any owners that cannot borrow from one scope, the same atomic, mutex, or immutable value, or a value that synchronizes itself, such as an HTTP client and its connection pool. It is a prelude name, like Box.

OperationResultEffect
Shared[T](value), Shared(value)Result[Shared[T], AllocError]Move value into one new allocation
handle.share()Shared[T]Another owner of the same value
handle.field, handle.method(...)as for TUse the content through a shared borrow
handle[i]elementCopy a copyable element; borrow an affine one with &handle[i]
&*handle&TBorrow the content
*handleTCopy copyable content
with handle.lock() as value:binds value: &mut VLock a Shared[Mutex[V]]; see mutexes

Construction makes one allocation, a counted box; failure drops the input and returns AllocError. share() increments the count and allocates nothing. Dropping a handle decrements it; the last drop runs T’s destructor once and frees the allocation. The count is atomic, so handles may be moved into, shared with, and dropped on any worker.

Access

A handle gives only shared access to its content. Every access borrows the content through *handle, and that loan is rooted at the handle’s binding: while a field reference, element reference, or &*handle is live, the handle cannot be moved, dropped, or reassigned. The handle may be a &Shared[T] reference; share() through it makes a new owner.

The content changes only through types that synchronize their own mutation: an Atomic, a Mutex, a channel endpoint, or a cancellation token. &mut *handle, assignment to a field or element, &mut self methods, and mutating collection methods are rejected. *handle copies copyable content; moving affine content out is rejected, since other owners may still use it.

Where a &T is expected, &handle converts to &T as a borrowed Box does: a &T parameter or an annotated r: &T = &handle borrows the content, and the loan is rooted at the handle as &*handle’s is. The same holds where a view is expected: &handle of a Shared[list[T]] is a &[T]. The conversion is shared only, so &mut handle for a &mut T parameter is rejected. There is no conversion from Shared[T] to T.

A temporary handle, such as Shared(5)?.share(), has no binding to root a borrow at and must be named first.

Rules

  • Handles have no equality, formatting, or print, including when stored inside other values. Compare or print values read through the handle.
  • copy() of a value that contains handles, such as a tuple, class, enum payload, list, or dictionary, copies its owned parts and gives each nested handle another owner, as share() would. The content is not copied and the count is shared. Sharing a handle allocates nothing; the copy allocates only for its owned parts. copy(handle) on a handle itself is rejected: write handle.share(). Slices copy their elements the same way.
  • T follows the storage rules of a box: no references, generators, or borrowed environments.
  • T cannot reach a ThreadPoolExecutor or Future. The last owner could drop on one of the pool’s own workers, which would then wait for its own pool.
  • T cannot reach Shared[T] again through a Mutex or a channel endpoint. Their contents change after construction, so a value could come to hold a share of itself and never be released. Immutable sharing, as in a tree or DAG of Shared nodes, is accepted: each node exists before any share of it. Link back to a parent or a sibling with an index into an owning list instead; see recursive data. There is no weak handle. For the same reason a BoxedCallable cannot be stored behind such a mutex or channel: its captures are not visible to the check.

Rules are checked at each construction, with the concrete T.

Threads

A handle crossing a thread boundary is checked like any other value: its content must be worker-eligible. Because the last owner can drop on any holder’s thread, the content’s destructors are checked as if every holder could run them. Mutexes reachable through a handle count for the compiler’s lock checks: a join, result wait, or pool shutdown while holding a lock on a Mutex[V] that a worker can reach through a share is rejected, and so is nesting a lock through a handle inside a lock on another Mutex[V] of the same V, since two handles can name one mutex.

Lowering

A handle is a pointer to the same counted record as a box: a header with an atomic reference count and destructor, the content’s type, and the content slot. share() is a relaxed atomic increment; a release decrements with release ordering, and the last owner acquires before destroying. Borrowing the content is an address computation. See the memory architecture.