-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Introduce reusable cancellable continuations for hot loops with channels #1534
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
Conversation
qwwdfsad
commented
Sep 13, 2019
- Average performance improvement is around 25%
- API is internal and targeted to specific usage
- DispatchedTask and DispatchedContinuation are extracted to separate files for better readability and maintainability
- Flow benchmark (temporary) rewritten to avoid take
* Average performance improvement is around 25% * API is internal and targeted to specific usage * DispatchedTask and DispatchedContinuation are extracted to separate files for better readability and maintainability * Flow benchmark (temporary) rewritten to avoid take
1144037
to
01c4e70
Compare
kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt
Outdated
Show resolved
Hide resolved
kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt
Outdated
Show resolved
Hide resolved
kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt
Outdated
Show resolved
Hide resolved
kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt
Outdated
Show resolved
Hide resolved
public override fun cancel(cause: Throwable?): Boolean { | ||
if (cancelLater(CancelledContinuation(this, cause, handled = state is CancelHandler))) { |
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.
There are several problems here. First of all, we create an expensive CancelledContinuation
and read state
every time here, for no particular reason, since the CancelledContinuation
instance will not be actually used (only cause
is needed). Moreover, this cancel is also called from checkCompleted
which does not need the call to cancelLater
here at all.
The second problem is that direct call to continuation.cancel
should not be deferred via cancelLater
. The test in ReusableCancellableContinuationTest.testNotCancelledOnClaimedResume
makes the wrong assumption and should be updated to reflect this fact.
Only notification from the parent via ChildContinuation
class needs to go through cancelLater
, so I'd suggest to remove cancelLater
from here completely. Instead, create a separate function to call it from ChildContinuation
:
internal fun parentCancelled(cause: Throwable) {
if (cancelLater(cause)) return
cancel(cause)
}
See a separate comment on reworking cancelLater
and postponeCancellation
so that it accepts cause: Throwable
as parameter.
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.
Nice idea with storing only the throwable.
The second problem is that direct call to continuation.cancel should not be deferred via
cancelLater
.
Fixed. I agree that now it is much simpler, though it now even more dangerous (moreover, it always was).
E.g. consider I am re-writing channels and now, for some reason, I start calling CancellableContinuation.cancel
directly (let's omit reusability at all).
In that case, our implementation (https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt#L561) will silently stop working in very rare concurrent cases.
Additional note: it also makes sense to use reusable continuations in semaphore and mutex implementation. Semaphore is used in |
…stead of CancelledContinuation in reusability SM, style fixes
kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt
Outdated
Show resolved
Hide resolved
kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt
Outdated
Show resolved
Hide resolved
…stead of CancelledContinuation in reusability SM, style fixes
Done, plus found one more potential leak, added the corresponding test. |
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'm happy with the general code now, sans the few comments. However, here is a general problem. This code leaks result of the last suspended coroutine for quite a prolonged time, since this result is retained in reusableCancellableContinuation.state
. This might be problematic in practice. I suggest to "clean up" the result after dispatching of reusable cancellable continuation and replace it with some "RESULT" symbol to ease the rest of implementation. To further simplify code, you can always clean it up (no need to check if it is reusable or not). There's no harm to clean up result of non-reusable continuation.
The best place to do it would be in CancellableContinuationImpl.takeState
, which is guaranteed to be invoked exactly once.
There's the exception of CompletedIdempotentResult
. This result cannot be cleaned up. You can deal with it in a different way, though. Instead of waiting for the next attempt to reuse a continuation, you can invalidate reusability state (set DC.state
to null
) immediately when CompletedIdempotentResult
is set in CCImpl.tryResume
, thus immediately detaching CCImpl
with CompletedIdempotentResult
from its parent DispatchedContinuation
and thus avoiding leak.
kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt
Outdated
Show resolved
Hide resolved
_reusableCancellableContinuation.loop { state -> | ||
// not when(state) to avoid Intrinsics.equals call | ||
when { | ||
state === REUSABLE_CLAIMED -> if (_reusableCancellableContinuation.compareAndSet(REUSABLE_CLAIMED, continuation)) return null |
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.
Still too long.
// not when(state) to avoid Intrinsics.equals call | ||
when { | ||
state === REUSABLE_CLAIMED -> if (_reusableCancellableContinuation.compareAndSet(REUSABLE_CLAIMED, continuation)) return null | ||
state === null -> return null |
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.
Actually, I cannot figure out an execution scenario where checkPostponedCancellation
is called at state == null
. It looks to to me that here we can only have REUSABLE_CLAIMED
or Throwable
. It cannot get invalidated while we are between claimRCC
and checkPC
calls and would stay either in REUSABLE_CLAIMED
or Throwable
state.
588a619
to
018a578
Compare
When a continuation is not reusable |
… the flow with test
018a578
to
03701ec
Compare