leanprover / leanprover/reference-manual
Thread synchronization overview
Nobody has claimed this yet.
- Dominant language
- Lean
- Stars
- 129
- Forks
- 67
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 16
Description
The documentation should include all this information that @hargoniX sent me, which covers synchronization tools but not async yet. Some of this is here, but the rest should be added and the overview should be more explicit.
ST.Ref/IO.Ref:
- Mutable memory cells that can be operated from within
STorBaseIO(and friends).- In the single threaded case they are simple mutable memory accesses
- In the multi threaded case they are implemented as spinlocks
- create new one with
mkRef get: take a local reference to the value from theRef, increasing its RC.set: put a new value intoRefswap: atomically replace the current value in theRefwith a new one and return the old valuetake: take the value that is currently inside of theRefand don't provide a replacement yet
(can be provided later withset). Crucially unlikegetthis operation does not increment the
RC of the value. Notably in the multi threaded case this function leaves the
spinlock locked until asethappens and should thus be used with caution (hence why it'sunsafe)ptrEq, check if twoRefpoint to the same mutable memory locationmodify/modifyGetact like monadicmodify/modifyGet, they:takethe value from theRef- run a function
fon it setthe resulting value (and potentially return it)
Crucially due to the spinlock nature ofRef,fshould never run long as this has the potential
to hang other threads while spinning
- Summary: Use either for mutable memory in single threaded cases or if you are working with very
short functions operating on a shared multi threaded state. If your functions are not short
consider one of the mutex variants
Every mutex variant comes in two forms Base and a data carrying variant. The base variant acts like
a classical lock while the data carrying variant has the data it carries with itself explicitly attached.
The most simple variant is just Std.BaseMutex which acts like a normal lock and provides:
newto create a new onelockto lock it, will block the current thread until theBaseMutexis available
(while technically UB due to the C++ implementation this will just deadlock if
called multiple times by the same thread without unlocking in between)tryLockto try and lock it right now, if it is already taken returnfalseinstantly otherwise
take the lock and returntrueunlockto unlock it
The data carrying variant of Std.BaseMutex is Std.Mutex:
newto create a new one with a certain initial piece of data associatedatomically kto run lock the mutex, runkwith access to the shared state and unlock it again afterwardstryAtomically kto try and lock the mutex, if available act likeatomically kand return the
value assome, if not available returnnone
Beyond this we have the Std.BaseSharedMutex and Std.SharedMutex. They provide a read-write lock
style functionality that is:
- there may be either up to infinitely many readers working on the shared state at a time
- xor exactly one writer (note that the writer may of course read the data as well as it has
exclusive access)
Std.BaseSharedMutex:
newto create a new onewriteto acquire write style access, will block until no writers and other readers are workingtryWriteto attempt to acquire a write style access, returntrueif possible without blocking,
falseotherwiseunlockWriteto relinquish write style access- same with
read/tryRead/unlockReadexcept that the condition is to block only until no
writers are working
The data carrying variant is Std.SharedMutex:
newto create a new one with a certain initial piece of data associatedatomically ktowritelock, runkthenunlockWritetryAtomically klike withStd.Mutexexcept that we take a write lock, note that the two
functions are intentionally named the same to enable drop-in replacement of aStd.Mutexwith a
SharedMutexatomicallyRead k/tryAtomicallyRead klikeatomically/tryAtomicallybut only acquire read
access and as such also only provide read access to the underlying data
Std.BaseRecursiveMutex and Std.RecursiveMutex are like Std.BaseMutex and Std.Mutex except
that they are reentrant/recursive locks, meaning that multiple calls to lock/atomically from the
same thread will not hang and instead just go through, they must however be met with a matching
amount of unlock when using the BaseRecursiveMutex variant in order to properly unlock again
(mutliple nested atomically of course just do that for you). Note that using this type of lock is
not popular with some people and you should probably try to avoid it in favor of better locking
schemes if possible.
Std.Condvar can be used with a BaseMutex/Mutex to provide a regular condition variable style
primitive, see the documentation at https://leanprover-community.github.io/mathlib4_docs/Std/Sync/Mutex.html#Std.Condvar
for a pretty exact description of the protocol. Condition variables are generally used in situations
where there is one thread that is waiting for a certain kind of condition (involving shared state
guarded by a mutex) to hold and other threads are working this shared state. The waiting thread can
use a condition variable that gets notified by other threads when they think the condition may
be true, the waiting thread then wakes up, locks the mutex, checks the condition and depending on
whether it holds or not unlocks and goes to sleep again or continues on with its work.
Std.Barrier can be used to provide a synchronization point for n threads:
newcreates a newBarrierthat acts as a synchronization point tonumThreadsthreads- calling
waitwill blocknumThreads - 1times before the caller wakes up all other threads and
they continue together. NotablyStd.Barrieris reusable so this type of rendezvous can happen
multiple times using the same barrier
Channels in lean can be configured along three axis
First closing behavior:
Std.CloseableChannelwhich is a channel that may be closed and thus both sending and receiving
on it may return an errorStd.Channelcannot be closed and thus sending and receiving can never throw an error
Second buffer size:
some 0: will create a channel that always acts as a synchronization point, that is everysend
will wait for a correspondingrecvto arrive and vice versasome n: will create a channel with an internal buffer of sizen, while there is less thann
elements waiting to be receivied asendwill never have to wait and can just enqueue its value
into the buffernone: will create an unbounded channel with a (potentially) everygrowing buffer. While this
means thatsendcan never block, having slightly faster senders than receivers can cause an
arbitrarily large memory consumption as values pile up. We thus recommend using either a zero
sized or buffered channel for almost all applications.
Third synchronization mode:
Std.Channel/Std.CloseableChannelwill returnIO.PromiseStd.Channel.Sync/Std.CloseableChannel.Syncwill make operations block the current thread if they need to wait
I will only cover the Sync variant here as they are relevant for programming without the async
stack being fully available yet.
A new channel may be created with Std.Channel.Sync.new/Std.CloseableChannel.Sync.new, passing
the capacity to it. For backwards compatability reasons the capacity has a default value of the
(undesirable) none. Both variants then provide functionality to:
send va valuev, potentially blocking if the buffer size of the channel does not allow this
right away. Note thatCloseableChannelmay throw an error here if the channel is already closedtrySend va valuev, returningtrueif this was possible right away or giving up and
returningfalseotherwiserecva value, potentially blocking if none is available right away. Note thatCloseableChannel
may returnnoneif the channel is already closed or gets closed while blocking.tryRecva value, returningsome vif a value was available right away ornoneif not- a
ForIninstance to indefinitely wait on new values on the channel using
for msg in ch do foo msg. NotablyCloseableChannelwill eventually abort this loop when the
channel is closed
Furthermore CloseableChannel offers:
closeto close the channel, note that this is not idempotent and will throw an error if
called more than once in order to enforce a well behaved cleanup behavior. Beyond this a call to
closewill make all currently blockingrecvreturnnoneisClosedto check if the channel is already closed (note that right after a call toisClosed
it may of course get closed so this is not sufficient as some sort of atomic check before closing
the channel)
As for the ordering behavior of the messages in the channel: It generally provides a FIFO style
behavior though of course in the presence of multiple producers/consumers this guarantee does not
mean too much.
important note regarding FBIP: Currently all ways of sharing a mutable state across threads
inherently prevent any in-place updates of datastructures, that is for example access to
Std.Mutex (Array Nat) within atomically will not be updated in place but instead copy.
This is a flaw that may be addressed in the future but for now we recommend not using FBIP
structures with synchronization primitives unless you know exactly what you are doing. (For example
maintaining a Vector of Ref Nat within a Std.Mutex and only updating the Ref will of course
still act in place and not copy Vector all the time as "the access is not really happening to the
Vector")
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by locating the synchronization overview in the reference manual and compare it with the tools and caveats listed in the issue. Cover the stated Ref, mutex, condition variable, barrier, and synchronous channel behavior, while keeping async coverage out of scope. Done means the overview explicitly describes these APIs, usage guidance, channel variants, and the FBIP warning.
Written by the indexing model from the issue text.
Assessment
- Domain
- documentation
- Issue type
- Documentation
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100