Skip to content

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

Merged
merged 4 commits into from
Sep 25, 2019

Conversation

qwwdfsad
Copy link
Collaborator

  • 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
@qwwdfsad qwwdfsad force-pushed the reuse-cancellable-continuations branch from 1144037 to 01c4e70 Compare September 13, 2019 11:42
public override fun cancel(cause: Throwable?): Boolean {
if (cancelLater(CancelledContinuation(this, cause, handled = state is CancelHandler))) {
Copy link
Contributor

@elizarov elizarov Sep 13, 2019

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.

Copy link
Collaborator Author

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.

@elizarov
Copy link
Contributor

Additional note: it also makes sense to use reusable continuations in semaphore and mutex implementation. Semaphore is used in flattenMerge and it should give a bit of improvement there.

…stead of CancelledContinuation in reusability SM, style fixes
@qwwdfsad qwwdfsad requested a review from elizarov September 18, 2019 16:14
…stead of CancelledContinuation in reusability SM, style fixes
@qwwdfsad
Copy link
Collaborator Author

qwwdfsad commented Sep 19, 2019

Done, plus found one more potential leak, added the corresponding test.
Waiting for stress tests to complete

@qwwdfsad qwwdfsad marked this pull request as ready for review September 19, 2019 11:43
Copy link
Contributor

@elizarov elizarov left a 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.

_reusableCancellableContinuation.loop { state ->
// not when(state) to avoid Intrinsics.equals call
when {
state === REUSABLE_CLAIMED -> if (_reusableCancellableContinuation.compareAndSet(REUSABLE_CLAIMED, continuation)) return null
Copy link
Contributor

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
Copy link
Contributor

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.

@qwwdfsad qwwdfsad force-pushed the reuse-cancellable-continuations branch from 588a619 to 018a578 Compare September 24, 2019 14:07
@qwwdfsad
Copy link
Collaborator Author

Actually, I cannot figure out an execution scenario where checkPostponedCancellation is called at state == null.

When a continuation is not reusable

@qwwdfsad qwwdfsad force-pushed the reuse-cancellable-continuations branch from 018a578 to 03701ec Compare September 24, 2019 14:09
@qwwdfsad qwwdfsad merged commit 946e578 into develop Sep 25, 2019
@qwwdfsad qwwdfsad deleted the reuse-cancellable-continuations branch September 25, 2019 15:02
elizarov added a commit that referenced this pull request Sep 27, 2019
elizarov added a commit that referenced this pull request Sep 27, 2019
elizarov added a commit that referenced this pull request Oct 1, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants