123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317open!Baseopen!Import(** See [Adaptive_backoff.once] *)letlog_scale=5modulePrim:Mutex_intf.Prim=structmoduleState:sig@@portable(** Implements primitive mutex state manipulation logic.
The state has three main features:
- a [locked] count,
- a (very large) threshold [locked] count that indicates that the mutex is locked
permanently, i.e. poisoned, and
- a bit that indicates whether a [release] operation should signal on the
awaitable to wake up awaiters.
State values are allowed to be momentarily imprecise:
- The [locked] count can be momentarily higher than [1] or (relatively speaking)
slightly higher than the (very large) threshold value of a permanently locked
mutex.
- The [locked] count of a permanently locked mutex may momentarily be (relatively
speaking) slightly less than the (very large) threshold value of a permanently
locked mutex.
This can be and is used to optimize both the [acquire] and [release] operations:
- Both [acquire] and [release] optimistically use [fetch_and_add]. This means that
in the common uncontended case they only perform a single atomic update of the
state without needing to first read the state.
- Although awaiters form a FIFO, active [acquire] callers are allowed to bypass
the FIFO. This avoids the lock convoy performance problem under heavy
contention.
- [release] does not wake up awaiters in case of contention as the state, i.e. the
[locked] count being more than one, allows it to detect when there is active
contention on the lock.
To make this possible, state changes are expressed as incremental updates to
current state that retain information from the previous state and actively
contending operations work cooperatively on the state:
- Both [acquire] and [release] may need to undo their increment or decrement,
respectively, of the [locked] count.
- The responsibility to signal awaiters, and update the state to indicate that
there are no awaiters, is transferred to whichever thread is the last to release
the mutex while there potentially are awaiters.
- Awoken awaiters must actively re-establish that there might be further awaiters. *)typet:immediate(** [unlocked_and_no_need_to_signal] is a state value constant indicating that there
are no awaiters on the mutex and that that the mutex is not locked. *)valunlocked_and_no_need_to_signal:t(** [unlocked_and_need_to_signal] is a state value constant indicating that there are
awaiters on the mutex and that the mutex is not locked. *)valunlocked_and_need_to_signal:t(* *)valincr_locked:tAwaitable.t@local->tvaldecr_locked:tAwaitable.t@local->t(* *)valequal:t->t->boolvalis_locked:t->boolvalis_locked_permanently:t->boolvalis_locked_at_most_once:t->boolvalhas_uncontended_awaiters:t->bool(* *)valand_locked_permanently:t->tvaland_need_to_signal:t->tvaland_decr_locked:t->tvaland_incr_locked:t->tend=structtypet=int(* The state looks like this:
{v
Bit: [ 0 | 1 to n-4 | n-3 | n-2 | n-1 ]
Use: [ no_need_to_signal | locked count | | locked permanently | 0 ]
v}
Above, [n] is the number of bits in an [int] and bit at [n-1] is the sign bit,
which should normally always be [0].
The [n-3] bit allows the [locked_permanently] bit to be temporarily [0] due to an
optimistic decrement of the [locked] count by [release]. *)(* [no_need_to_signal] indicates whether or not there are potential awaiters. *)letno_need_to_signal=1lsl0letunlocked_and_no_need_to_signal=no_need_to_signalletunlocked_and_need_to_signal=0(* [locked] is the value [1] of counter bits. The counter is manipulated using
[fetch_and_add] and may temporarily be greater than [locked].
There is no fear of overflow, because the number of extra increments is bounded by
the number of contending threads and there should be far more than enough bits in
an int to not overflow. *)letlocked=1lsl1(* [locked_permanently] is a threshold value for a poisoned mutex.
When it comes to the threshold, we allow both lower and higher values so that both
[acquire] and [release] can use [fetch_and_add] and then check and potentially
revert the state later. *)letlocked_permanently=1lsl(Int.num_bits-2)let[@inline]incr_lockedt=Awaitable.fetch_and_addtlockedlet[@inline]decr_lockedt=Awaitable.fetch_and_addt(-locked)(* *)letequal=Int.equallet[@inline]is_lockedstate=locked<=statelet[@inline]is_locked_at_most_oncestate=state<locked*2let[@inline]has_uncontended_awaitersstate=(* This is [true] when the [no_need_to_signal] bit has been cleared, meaning that
there are awaiters, and no other thread has temporarily incremented the [locked]
count. This is the case where we need to signal an awaiter. In other cases we
don't need to signal, because either there are no awaiters or there are threads
contending for the mutex and one of them will notice the lock has been released. *)state=locked;;let[@inline]is_locked_permanentlystate=(* This allows lower values so that [release] may optimistically use [fetch_and_add]
and revert the counter decrement later in case the mutex was locked permanently. *)locked_permanently/2<=state;;let[@inline]and_locked_permanentlystate=state+locked_permanentlylet[@inline]and_need_to_signalstate=statelandlnotno_need_to_signallet[@inline]and_decr_lockedstate=state-lockedlet[@inline]and_incr_lockedstate=state+lockedendtypet=State.tAwaitable.tlet[@inlinenever]recpoisont=letbefore=Awaitable.gettinifnot(State.is_locked_permanentlybefore)then(matchAwaitable.compare_and_sett~if_phys_equal_to:before~replace_with:(before|>State.and_locked_permanently)with|Set_here->Awaitable.broadcastt|Compare_failed->poisont);;type('a,_)result=|Value:('a,'a)result|Or_canceled:('a,'aOr_canceled.t)resultlet[@inlinenever]acquire_as(typer)wct(r:(unit,r)result):r=let[@inline]completed():r=matchrwith|Value->()|Or_canceled->Completed()inletprior=State.incr_lockedtinifnot(State.is_lockedprior)thencompleted()else(let[@inline]recacquire_awaitingwctbefore=ifnot(State.is_lockedbefore)then((* We know [before] is [no_need_to_signal] or [awaiters]. *)letafter=State.unlocked_and_need_to_signal|>State.and_incr_lockedinmatchAwaitable.compare_and_sett~if_phys_equal_to:before~replace_with:afterwith|Set_here->completed()|Compare_failed->Adaptive_backoff.once~random_key:(Awaitable.random_keyt)~log_scale;acquire_awaitingwct(Awaitable.gett))elseifState.is_locked_permanentlybeforethenraisePoisonedelse(letafter=before|>State.and_need_to_signalinifphys_equalbeforeafter||matchAwaitable.compare_and_sett~if_phys_equal_to:before~replace_with:afterwith|Set_here->true|Compare_failed->falsethen(matchrwith|Value->(matchAwaitable.awaitwt~until_phys_unequal_to:afterwith|Signaled->acquire_awaitingwct(Awaitable.gett)|Terminated->raiseAwait.Terminated)|Or_canceled->(matchAwaitable.await_or_cancelwct~until_phys_unequal_to:afterwith|Signaled->acquire_awaitingwct(Awaitable.gett)|Terminated->raiseAwait.Terminated|Canceled->Or_canceled.Canceled))else(Adaptive_backoff.once~random_key:(Awaitable.random_keyt)~log_scale;acquire_awaitingwct(Awaitable.gett)))inlet[@inline]recacquire_contendedwctbefore=(* At this point we have incremented the [locked] count.
We now check whether we actually managed to obtain the mutex exclusively or
whether we should try to decrement the count back and await. *)ifState.is_locked_at_most_oncebeforethencompleted()else(letafter=before|>State.and_decr_locked|>State.and_need_to_signalinmatchAwaitable.compare_and_sett~if_phys_equal_to:before~replace_with:afterwith|Set_here->acquire_awaitingwctafter|Compare_failed->Adaptive_backoff.once~random_key:(Awaitable.random_keyt)~log_scale;acquire_contendedwct(Awaitable.gett))inacquire_contendedwct(prior|>State.and_incr_locked));;letacquire_or_cancelwct=acquire_aswctOr_canceledletacquirewt=acquire_aswCancellation.nevertValuelettry_acquiret=let[@inlinenever]failedtstate=ifState.is_locked_permanentlystatethenraisePoisoned;(* Backoff in case [try_acquire] is called in a loop. *)Adaptive_backoff.once~random_key:(Awaitable.random_keyt)~log_scale;falseinletstate=Awaitable.gettinifState.is_lockedstatethenfailedtstateelse(letprior=Awaitable.compare_exchanget~if_phys_equal_to:state~replace_with:(State.and_incr_lockedstate)inifState.equalstatepriorthentrueelsefailedtprior);;let[@inline]signal_awaitert=matchAwaitable.compare_and_sett~if_phys_equal_to:State.unlocked_and_need_to_signal~replace_with:State.unlocked_and_no_need_to_signalwith|Set_here->Awaitable.signalt|Compare_failed->();;let[@inlinenever]releaset=let[@inline]undo_releaset=(* This may seem unnecessary, however, if the mutex was poisoned during
[Condition.wait] or [release_temporarily] then [release] may be called from an
exception handler on a non-[acquire]d mutex. *)let_:State.t=State.incr_lockedtin()in(* At this point we assume we have previously incremented [locked] count. *)letprior=State.decr_lockedtinifState.has_uncontended_awaiterspriorthensignal_awaitertelseifState.is_locked_permanentlypriorthenundo_releaset;;let[@inlinenever]release_and_reraiseexnt=letbt=Backtrace.Exn.most_recent()inreleaset;Exn.raise_with_original_backtraceexnbt;;let[@inlinenever]poison_and_reraiseexnt=letbt=Backtrace.Exn.most_recent()inpoisont;Exn.raise_with_original_backtraceexnbt;;let[@inline]create?padded()=Awaitable.make?paddedState.unlocked_and_no_need_to_signal;;letis_poisonedt=State.is_locked_permanently(Awaitable.gett)letis_lockedt=State.is_locked(Awaitable.gett)moduleFor_testing=structletis_exclusive=is_lockedincludeAwaitable.For_testingendendmoduleSync=Mutex_common.Make_sync(Prim)moduleAwait=Mutex_common.Make_await(Prim)