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.
| Operation | Result | Effect |
|---|---|---|
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 T | Use the content through a shared borrow |
handle[i] | element | Copy a copyable element; borrow an affine one with &handle[i] |
&*handle | &T | Borrow the content |
*handle | T | Copy copyable content |
with handle.lock() as value: | binds value: &mut V | Lock 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, asshare()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: writehandle.share(). Slices copy their elements the same way.Tfollows the storage rules of a box: no references, generators, or borrowed environments.Tcannot reach aThreadPoolExecutororFuture. The last owner could drop on one of the pool’s own workers, which would then wait for its own pool.Tcannot reachShared[T]again through aMutexor 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 ofSharednodes, 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 aBoxedCallablecannot 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.