Explain the thread-safety and single-resume guarantees of CancellableContinuation when bridging a callback that can fire from multiple threads.
answer
- Atomic single-winner completion
- Resume is thread-safe + re-dispatches to coroutine context
- Double resume still throws IllegalStateException
- Guard with isActive or tryResume
- Make cancellation/callback paths idempotent
basics
~20 sA continuation must be resumed exactly once. Resume is thread-safe, but if a callback might fire success and error, or fire twice, you must guard so only the first resume wins and you do not throw on the second.
solid answer
~50 sCancellableContinuation guarantees the coroutine is resumed at most once: the internal state is atomic, so resume / resumeWithException are safe to call from any thread and the continuation will dispatch back onto its own context. The danger is your callback contract: many callback APIs may invoke both onSuccess and onError, may retry, or may race with cancellation. A second resume on an already-resumed continuation throws IllegalStateException. So you either (a) guard with cont.isActive before resuming, or (b) use a tryResume-style atomic check, or (c) ensure the callback fires once by contract. invokeOnCancellation can also race with the callback — both can be invoked near-simultaneously, so make both idempotent: the cleanup and the resume-onCancellation path must tolerate being called regardless of order. Never assume the callback thread equals the coroutine thread; the continuation handles the dispatch.
go deeper
Knows resume should happen once.
Guards resume with isActive against a double-firing callback.
Explains atomic single-winner semantics, thread-safe resume, dispatch, and the cancellation race.
Designs robust bridges with tryResume/completeResume and idempotent teardown for hostile callback contracts.
## What the continuation guarantees A `CancellableContinuation<T>` is backed by **atomic state**. That gives you: - **Resume at most once.** Internally a CAS decides the single winning completion (a value, an exception, or cancellation). - **Thread-safe resume.** You may call `resume` / `resumeWithException` from *any* thread; the continuation re-dispatches the coroutine onto its own `CoroutineContext` (its dispatcher), so your code resumes where it expects. ## What it does NOT protect against The guarantee is *at most one completion wins*, but a **second resume still throws `IllegalStateException`**. Real callback APIs are messy: - They may call both `onSuccess` and `onError`. - They may fire more than once (retries, re-deliveries). - The success callback may race with **cancellation**. Your bridge must make the *caller* side single-shot. ## Patterns to stay safe ```kotlin suspendCancellableCoroutine { cont -> api.subscribe(object : Listener { override fun onValue(v: T) { if (cont.isActive) cont.resume(v) // guard double-fire } override fun onError(e: Throwable) { if (cont.isActive) cont.resumeWithException(e) } }) cont.invokeOnCancellation { api.unsubscribe() } } ``` - `cont.isActive` is `false` once resumed or cancelled — a cheap guard against double-fire. - For a strict atomic check you can use the low-level `tryResume`/`tryResumeWithException` API and `completeResume`, but `isActive` is enough for most callbacks. ## The cancellation race `invokeOnCancellation` and the callback can run **at nearly the same time** on different threads. Both paths must be **idempotent**: - Cleanup (`unsubscribe`, `call.cancel()`) must be safe to call even if work already finished. - A resumed value that loses to cancellation should be released via `cont.resume(value) { value.close() }`. ## Dispatch, not thread inheritance Do not assume the coroutine continues on the callback's thread. The continuation captures the coroutine's context at suspension and dispatches back through it. So heavy work in the callback before `resume` runs on the callback thread, not the coroutine's dispatcher. ## Summary - Continuation: atomic, single-winner, thread-safe resume, context-correct dispatch. - You own: making the callback effectively single-shot (`isActive`/`tryResume`). - Make cleanup and resume-vs-cancel idempotent.
- When would you reach for tryResume instead of isActive?When you need a fully atomic claim-and-resume (the value carries resources or two callbacks can race), tryResume returns a token you confirm with completeResume so only one resumption commits.
- Does resume run your continuation on the callback thread?No; the continuation dispatches back through the coroutine's own context/dispatcher captured at suspension.
saying these in an interview costs you the question
- Assumes resume is unsafe to call off the coroutine thread
- Relies on the callback firing exactly once without guarding
- Thinks isActive eliminates all races without idempotent cleanup
- Believes the coroutine resumes on the callback's thread
- Catches and swallows the IllegalStateException from double resume instead of preventing it