-
Notifications
You must be signed in to change notification settings - Fork 521
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Even faster async mutex #3409
Even faster async mutex #3409
Changes from 8 commits
01ca653
e1ac43f
68b50b5
2ecd790
1e4176b
989277e
95bbcba
2874d3e
399544b
2ccf8be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,7 @@ package std | |
import cats.effect.kernel._ | ||
import cats.syntax.all._ | ||
|
||
import java.util.concurrent.atomic.AtomicReference | ||
import java.util.concurrent.atomic.AtomicBoolean | ||
|
||
/** | ||
* A purely functional mutex. | ||
|
@@ -84,9 +84,7 @@ object Mutex { | |
* Creates a new `Mutex`. Like `apply` but initializes state using another effect constructor. | ||
*/ | ||
def in[F[_], G[_]](implicit F: Sync[F], G: Async[G]): F[Mutex[G]] = | ||
F.delay( | ||
new AtomicReference[LockCell]() | ||
).map(state => new AsyncImpl[G](state)(G)) | ||
F.delay(new AsyncImpl[G]) | ||
|
||
private final class ConcurrentImpl[F[_]](sem: Semaphore[F]) extends Mutex[F] { | ||
override final val lock: Resource[F, Unit] = | ||
|
@@ -96,92 +94,65 @@ object Mutex { | |
new ConcurrentImpl(sem.mapK(f)) | ||
} | ||
|
||
private final class AsyncImpl[F[_]](state: AtomicReference[LockCell])(implicit F: Async[F]) | ||
extends Mutex[F] { | ||
// Cancels a Fiber waiting for the Mutex. | ||
private def cancel(thisCB: CB, thisCell: LockCell, previousCell: LockCell): F[Unit] = | ||
F.delay { | ||
// If we are canceled. | ||
// First, we check if the state still contains ourselves, | ||
// if that is the case, we swap it with the previousCell. | ||
// This ensures any consequent attempt to acquire the Mutex | ||
// will register its callback on the appropriate cell. | ||
// Additionally, that confirms there is no Fiber | ||
// currently waiting for us. | ||
if (!state.compareAndSet(thisCell, previousCell)) { | ||
// Otherwise, | ||
// it means we have a Fiber waiting for us. | ||
// Thus, we need to tell the previous cell | ||
// to awake that Fiber instead. | ||
var nextCB = thisCell.get() | ||
while (nextCB eq null) { | ||
// There is a tiny fraction of time when | ||
// the next cell has acquired ourselves, | ||
// but hasn't registered itself yet. | ||
// Thus, we spin loop until that happens | ||
nextCB = thisCell.get() | ||
} | ||
if (!previousCell.compareAndSet(thisCB, nextCB)) { | ||
// However, in case the previous cell had already completed, | ||
// then the Mutex is free and we can awake our waiting fiber. | ||
if (nextCB ne null) nextCB.apply(Either.unit) | ||
} | ||
} | ||
} | ||
private final class AsyncImpl[F[_]](implicit F: Async[F]) extends Mutex[F] { | ||
import AsyncImpl._ | ||
|
||
// Awaits until the Mutex is free. | ||
private def await(thisCell: LockCell): F[Unit] = | ||
F.asyncCheckAttempt[Unit] { thisCB => | ||
F.delay { | ||
val previousCell = state.getAndSet(thisCell) | ||
private[this] val locked = new AtomicBoolean(false) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we swap this |
||
private[this] val waiters = new UnsafeUnbounded[Either[Throwable, Boolean] => Unit] | ||
|
||
if (previousCell eq null) { | ||
// If the previous cell was null, | ||
// then the Mutex is free. | ||
Either.unit | ||
private[this] val acquire: F[Unit] = F | ||
.asyncCheckAttempt[Boolean] { cb => | ||
F.delay { | ||
if (locked.compareAndSet(false, true)) { // acquired | ||
RightTrue | ||
} else { | ||
// Otherwise, | ||
// we check again that the previous cell haven't been completed yet, | ||
// if not we tell the previous cell to awake us when they finish. | ||
if (!previousCell.compareAndSet(null, thisCB)) { | ||
// If it was already completed, | ||
// then the Mutex is free. | ||
Either.unit | ||
val cancel = waiters.put(cb) | ||
if (locked.compareAndSet(false, true)) { // try again | ||
cancel() | ||
RightTrue | ||
} else { | ||
Left(Some(cancel(thisCB, thisCell, previousCell))) | ||
Left(Some(F.delay(cancel()))) | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Acquires the Mutex. | ||
private def acquire(poll: Poll[F]): F[LockCell] = | ||
F.delay(new AtomicReference[CB]()).flatMap { thisCell => | ||
poll(await(thisCell).map(_ => thisCell)) | ||
.flatMap { acquired => | ||
if (acquired) F.unit // home free | ||
else acquire // wokened, but need to acquire | ||
} | ||
|
||
// Releases the Mutex. | ||
private def release(thisCell: LockCell): F[Unit] = | ||
F.delay { | ||
// If the state still contains our own cell, | ||
// then it means nobody was waiting for the Mutex, | ||
// and thus it can be put on a free state again. | ||
if (!state.compareAndSet(thisCell, null)) { | ||
// Otherwise, | ||
// our cell is probably not empty, | ||
// we must awake whatever Fiber is waiting for us. | ||
val nextCB = thisCell.getAndSet(Sentinel) | ||
if (nextCB ne null) nextCB.apply(Either.unit) | ||
} | ||
private[this] val _release: F[Unit] = F.delay { | ||
try { // look for a waiter | ||
var waiter = waiters.take() | ||
while (waiter eq null) waiter = waiters.take() | ||
waiter(RightTrue) // pass the buck | ||
} catch { // no waiter found | ||
case FailureSignal => | ||
locked.set(false) // release | ||
try { | ||
var waiter = waiters.take() | ||
while (waiter eq null) waiter = waiters.take() | ||
waiter(RightFalse) // waken any new waiters | ||
} catch { | ||
Comment on lines
+130
to
+137
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's some fairness corruption here under contention, where an acquirer may cut-in-line of an acquirer that had placed itself in the queue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are the FIFO semantics of the current BTW, does the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overall it's still FIFO (or should be, we should add a test if we don't have already). It's just under contention it can be slightly corrupted—but under contention, who was really "first" anyway? I think this was the long-running debate Daniel and Fabio had for the async queue :) |
||
case FailureSignal => // do nothing | ||
} | ||
} | ||
} | ||
|
||
private[this] val release: Unit => F[Unit] = _ => _release | ||
|
||
override final val lock: Resource[F, Unit] = | ||
Resource.makeFull[F, LockCell](acquire)(release).void | ||
Resource.makeFull[F, Unit](poll => poll(acquire))(release) | ||
|
||
override def mapK[G[_]](f: F ~> G)(implicit G: MonadCancel[G, _]): Mutex[G] = | ||
new Mutex.TransformedMutex(this, f) | ||
} | ||
|
||
private object AsyncImpl { | ||
private val RightTrue = Right(true) | ||
private val RightFalse = Right(false) | ||
} | ||
|
||
private final class TransformedMutex[F[_], G[_]]( | ||
underlying: Mutex[F], | ||
f: F ~> G | ||
|
@@ -194,9 +165,4 @@ object Mutex { | |
new Mutex.TransformedMutex(this, f) | ||
} | ||
|
||
private type CB = Either[Throwable, Unit] => Unit | ||
|
||
private final val Sentinel: CB = _ => () | ||
|
||
private type LockCell = AtomicReference[CB] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder what is the rationale for this change? And why only to the happy path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I misunderstood the purpose of the benchmark, but we want to replicate many acquire/releases of the same mutex from the same fiber—we don't need to allocate a new mutex in each iteration, and "fibers" is not really accurate term. It's just iterations in the end.
Actually you are right, we can probably make a similar change to the other benchmarks.