{
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-context-09.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-context-09.kt).
The output of this code with the `-Dkotlinx.coroutines.debug` JVM option is:
@@ -501,21 +529,8 @@ class Activity {
-Alternatively, we can implement the [CoroutineScope] interface in this `Activity` class. The best way to do it is
-to use delegation with default factory functions.
-We also can combine the desired dispatcher (we used [Dispatchers.Default] in this example) with the scope:
-
-
-
-```kotlin
- class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {
- // to be continued ...
-```
-
-
-
-Now, we can launch coroutines in the scope of this `Activity` without having to explicitly
-specify their context. For the demo, we launch ten coroutines that delay for a different time:
+Now, we can launch coroutines in the scope of this `Activity` using the defined `scope`.
+For the demo, we launch ten coroutines that delay for a different time:
@@ -524,7 +539,7 @@ specify their context. For the demo, we launch ten coroutines that delay for a d
fun doSomething() {
// launch ten coroutines for a demo, each working for a different time
repeat(10) { i ->
- launch {
+ mainScope.launch {
delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
println("Coroutine $i is done")
}
@@ -544,21 +559,19 @@ of the activity no more messages are printed, even if we wait a little longer.
```kotlin
-import kotlin.coroutines.*
import kotlinx.coroutines.*
-class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {
-
+class Activity {
+ private val mainScope = CoroutineScope(Dispatchers.Default) // use Default for test purposes
+
fun destroy() {
- cancel() // Extension on CoroutineScope
+ mainScope.cancel()
}
- // to be continued ...
- // class Activity continues
fun doSomething() {
// launch ten coroutines for a demo, each working for a different time
repeat(10) { i ->
- launch {
+ mainScope.launch {
delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
println("Coroutine $i is done")
}
@@ -581,7 +594,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-context-10.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-context-10.kt).
The output of this example is:
@@ -597,6 +610,9 @@ Destroying activity!
As you can see, only the first two coroutines print a message and the others are cancelled
by a single invocation of `job.cancel()` in `Activity.destroy()`.
+> Note, that Android has first-party support for coroutine scope in all entities with the lifecycle.
+See [the corresponding documentation](https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescope).
+
### Thread-local data
Sometimes it is convenient to have an ability to pass some thread-local data to or between coroutines.
@@ -632,12 +648,12 @@ fun main() = runBlocking
{
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-context-11.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-context-11.kt).
In this example we launch a new coroutine in a background thread pool using [Dispatchers.Default], so
it works on a different thread from the thread pool, but it still has the value of the thread local variable
that we specified using `threadLocal.asContextElement(value = "launch")`,
-no matter on what thread the coroutine is executed.
+no matter which thread the coroutine is executed on.
Thus, the output (with [debug](#debugging-coroutines-and-threads)) is:
```text
@@ -664,7 +680,7 @@ stored in a thread-local variable. However, in this case you are fully responsib
potentially concurrent modifications to the variable in this mutable box.
For advanced usage, for example for integration with logging MDC, transactional contexts or any other libraries
-which internally use thread-locals for passing data, see documentation of the [ThreadContextElement] interface
+which internally use thread-locals for passing data, see the documentation of the [ThreadContextElement] interface
that should be implemented.
diff --git a/docs/coroutines-guide.md b/docs/coroutines-guide.md
index e3f18d208e..2d15a7bbff 100644
--- a/docs/coroutines-guide.md
+++ b/docs/coroutines-guide.md
@@ -10,7 +10,7 @@ coroutine-enabled primitives that this guide covers, including `launch`, `async`
This is a guide on core features of `kotlinx.coroutines` with a series of examples, divided up into different topics.
-In order to use coroutines as well as follow the examples in this guide, you need to add a dependency on `kotlinx-coroutines-core` module as explained
+In order to use coroutines as well as follow the examples in this guide, you need to add a dependency on the `kotlinx-coroutines-core` module as explained
[in the project README](../README.md#using-in-your-projects).
## Table of contents
diff --git a/docs/exception-handling.md b/docs/exception-handling.md
index 08e63ea994..a3070213d1 100644
--- a/docs/exception-handling.md
+++ b/docs/exception-handling.md
@@ -18,22 +18,22 @@
## Exception Handling
-
This section covers exception handling and cancellation on exceptions.
-We already know that cancelled coroutine throws [CancellationException] in suspension points and that it
-is ignored by coroutines machinery. But what happens if an exception is thrown during cancellation or multiple children of the same
-coroutine throw an exception?
+We already know that a cancelled coroutine throws [CancellationException] in suspension points and that it
+is ignored by the coroutines' machinery. Here we look at what happens if an exception is thrown during cancellation or multiple children of the same
+coroutine throw an exception.
### Exception propagation
-Coroutine builders come in two flavors: propagating exceptions automatically ([launch] and [actor]) or
+Coroutine builders come in two flavors: propagating exceptions automatically ([launch] and [actor]) or
exposing them to users ([async] and [produce]).
-The former treat exceptions as unhandled, similar to Java's `Thread.uncaughtExceptionHandler`,
-while the latter are relying on the user to consume the final
+When these builders are used to create a _root_ coroutine, that is not a _child_ of another coroutine,
+the former builders treat exceptions as **uncaught** exceptions, similar to Java's `Thread.uncaughtExceptionHandler`,
+while the latter are relying on the user to consume the final
exception, for example via [await][Deferred.await] or [receive][ReceiveChannel.receive]
([produce] and [receive][ReceiveChannel.receive] are covered later in [Channels](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/channels.md) section).
-It can be demonstrated by a simple example that creates coroutines in the [GlobalScope]:
+It can be demonstrated by a simple example that creates root coroutines using the [GlobalScope]:
@@ -41,13 +41,13 @@ It can be demonstrated by a simple example that creates coroutines in the [Globa
import kotlinx.coroutines.*
fun main() = runBlocking {
- val job = GlobalScope.launch {
+ val job = GlobalScope.launch { // root coroutine with launch
println("Throwing exception from launch")
throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
}
job.join()
println("Joined failed job")
- val deferred = GlobalScope.async {
+ val deferred = GlobalScope.async { // root coroutine with async
println("Throwing exception from async")
throw ArithmeticException() // Nothing is printed, relying on user to call await
}
@@ -62,7 +62,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-exceptions-01.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-exceptions-01.kt).
The output of this code is (with [debug](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/coroutine-context-and-dispatchers.md#debugging-coroutines-and-threads)):
@@ -78,9 +78,13 @@ Caught ArithmeticException
### CoroutineExceptionHandler
-But what if one does not want to print all exceptions to the console?
-[CoroutineExceptionHandler] context element is used as generic `catch` block of coroutine where custom logging or exception handling may take place.
-It is similar to using [`Thread.uncaughtExceptionHandler`](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setUncaughtExceptionHandler(java.lang.Thread.UncaughtExceptionHandler)).
+It is possible to customize the default behavior of printing **uncaught** exceptions to the console.
+[CoroutineExceptionHandler] context element on a _root_ coroutine can be used as generic `catch` block for
+this root coroutine and all its children where custom exception handling may take place.
+It is similar to [`Thread.uncaughtExceptionHandler`](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setUncaughtExceptionHandler(java.lang.Thread.UncaughtExceptionHandler)).
+You cannot recover from the exception in the `CoroutineExceptionHandler`. The coroutine had already completed
+with the corresponding exception when the handler is called. Normally, the handler is used to
+log the exception, show some kind of error message, terminate, and/or restart the application.
On JVM it is possible to redefine global exception handler for all coroutines by registering [CoroutineExceptionHandler] via
[`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html).
@@ -89,8 +93,15 @@ Global exception handler is similar to
which is used when no more specific handlers are registered.
On Android, `uncaughtExceptionPreHandler` is installed as a global coroutine exception handler.
-[CoroutineExceptionHandler] is invoked only on exceptions which are not expected to be handled by the user,
-so registering it in [async] builder and the like of it has no effect.
+`CoroutineExceptionHandler` is invoked only on **uncaught** exceptions — exceptions that were not handled in any other way.
+In particular, all _children_ coroutines (coroutines created in the context of another [Job]) delegate handling of
+their exceptions to their parent coroutine, which also delegates to the parent, and so on until the root,
+so the `CoroutineExceptionHandler` installed in their context is never used.
+In addition to that, [async] builder always catches all exceptions and represents them in the resulting [Deferred] object,
+so its `CoroutineExceptionHandler` has no effect either.
+
+> Coroutines running in supervision scope do not propagate exceptions to their parent and are
+excluded from this rule. A further [Supervision](#supervision) section of this document gives more details.
@@ -100,12 +111,12 @@ import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _, exception ->
- println("Caught $exception")
+ println("CoroutineExceptionHandler got $exception")
}
- val job = GlobalScope.launch(handler) {
+ val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
throw AssertionError()
}
- val deferred = GlobalScope.async(handler) {
+ val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
}
joinAll(job, deferred)
@@ -115,19 +126,19 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-exceptions-02.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-exceptions-02.kt).
The output of this code is:
```text
-Caught java.lang.AssertionError
+CoroutineExceptionHandler got java.lang.AssertionError
```
### Cancellation and exceptions
-Cancellation is tightly bound with exceptions. Coroutines internally use `CancellationException` for cancellation, these
+Cancellation is closely related to exceptions. Coroutines internally use `CancellationException` for cancellation, these
exceptions are ignored by all handlers, so they should be used only as the source of additional debug information, which can
be obtained by `catch` block.
When a coroutine is cancelled using [Job.cancel], it terminates, but it does not cancel its parent.
@@ -161,7 +172,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-exceptions-03.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-exceptions-03.kt).
The output of this code is:
@@ -175,15 +186,17 @@ Parent is not cancelled
If a coroutine encounters an exception other than `CancellationException`, it cancels its parent with that exception.
This behaviour cannot be overridden and is used to provide stable coroutines hierarchies for
-[structured concurrency](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/composing-suspending-functions.md#structured-concurrency-with-async) which do not depend on
-[CoroutineExceptionHandler] implementation.
-The original exception is handled by the parent when all its children terminate.
+[structured concurrency](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/composing-suspending-functions.md#structured-concurrency-with-async).
+[CoroutineExceptionHandler] implementation is not used for child coroutines.
-> This also a reason why, in these examples, [CoroutineExceptionHandler] is always installed to a coroutine
+> In these examples [CoroutineExceptionHandler] is always installed to a coroutine
that is created in [GlobalScope]. It does not make sense to install an exception handler to a coroutine that
is launched in the scope of the main [runBlocking], since the main coroutine is going to be always cancelled
when its child completes with exception despite the installed handler.
+The original exception is handled by the parent only when all its children terminate,
+which is demonstrated by the following example.
+
```kotlin
@@ -192,7 +205,7 @@ import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _, exception ->
- println("Caught $exception")
+ println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
launch { // the first child
@@ -219,7 +232,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-exceptions-04.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-exceptions-04.kt).
The output of this code is:
@@ -227,22 +240,15 @@ The output of this code is:
Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
-Caught java.lang.ArithmeticException
+CoroutineExceptionHandler got java.lang.ArithmeticException
```
### Exceptions aggregation
-What happens if multiple children of a coroutine throw an exception?
-The general rule is "the first exception wins", so the first thrown exception is exposed to the handler.
-But that may cause lost exceptions, for example if coroutine throws an exception in its `finally` block.
-So, additional exceptions are suppressed.
-
-> One of the solutions would have been to report each exception separately,
-but then [Deferred.await] should have had the same mechanism to avoid behavioural inconsistency and this
-would cause implementation details of a coroutines (whether it had delegated parts of its work to its children or not)
-to leak to its exception handler.
-
+When multiple children of a coroutine fail with an exception, the
+general rule is "the first exception wins", so the first exception gets handled.
+All additional exceptions that happen after the first one are attached to the first exception as suppressed ones.
-> Note, this mechanism currently works only on Java version 1.7+.
-Limitation on JS and Native is temporary and will be fixed in the future.
+> Note that this mechanism currently only works on Java version 1.7+.
+The JS and Native restrictions are temporary and will be lifted in the future.
-Cancellation exceptions are transparent and unwrapped by default:
+Cancellation exceptions are transparent and are unwrapped by default:
@@ -304,13 +310,13 @@ import java.io.*
fun main() = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _, exception ->
- println("Caught original $exception")
+ println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
- val inner = launch {
+ val inner = launch { // all this stack of coroutines will get cancelled
launch {
launch {
- throw IOException()
+ throw IOException() // the original exception
}
}
}
@@ -318,7 +324,7 @@ fun main() = runBlocking {
inner.join()
} catch (e: CancellationException) {
println("Rethrowing CancellationException with original cause")
- throw e
+ throw e // cancellation exception is rethrown, yet the original IOException gets to the handler
}
}
job.join()
@@ -328,32 +334,33 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-exceptions-06.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-exceptions-06.kt).
The output of this code is:
```text
Rethrowing CancellationException with original cause
-Caught original java.io.IOException
+CoroutineExceptionHandler got java.io.IOException
```
### Supervision
As we have studied before, cancellation is a bidirectional relationship propagating through the whole
-coroutines hierarchy. But what if unidirectional cancellation is required?
+hierarchy of coroutines. Let us take a look at the case when unidirectional cancellation is required.
A good example of such a requirement is a UI component with the job defined in its scope. If any of the UI's child tasks
have failed, it is not always necessary to cancel (effectively kill) the whole UI component,
-but if UI component is destroyed (and its job is cancelled), then it is necessary to fail all child jobs as their results are no longer required.
+but if UI component is destroyed (and its job is cancelled), then it is necessary to fail all child jobs as their results are no longer needed.
-Another example is a server process that spawns several children jobs and needs to _supervise_
-their execution, tracking their failures and restarting just those children jobs that had failed.
+Another example is a server process that spawns multiple child jobs and needs to _supervise_
+their execution, tracking their failures and only restarting the failed ones.
#### Supervision job
-For these purposes [SupervisorJob][SupervisorJob()] can be used. It is similar to a regular [Job][Job()] with the only exception that cancellation is propagated
-only downwards. It is easy to demonstrate with an example:
+The [SupervisorJob][SupervisorJob()] can be used for these purposes.
+It is similar to a regular [Job][Job()] with the only exception that cancellation is propagated
+only downwards. This can easily be demonstrated using the following example:
@@ -365,24 +372,24 @@ fun main() = runBlocking {
with(CoroutineScope(coroutineContext + supervisor)) {
// launch the first child -- its exception is ignored for this example (don't do this in practice!)
val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {
- println("First child is failing")
- throw AssertionError("First child is cancelled")
+ println("The first child is failing")
+ throw AssertionError("The first child is cancelled")
}
// launch the second child
val secondChild = launch {
firstChild.join()
// Cancellation of the first child is not propagated to the second child
- println("First child is cancelled: ${firstChild.isCancelled}, but second one is still active")
+ println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
try {
delay(Long.MAX_VALUE)
} finally {
// But cancellation of the supervisor is propagated
- println("Second child is cancelled because supervisor is cancelled")
+ println("The second child is cancelled because the supervisor was cancelled")
}
}
// wait until the first child fails & completes
firstChild.join()
- println("Cancelling supervisor")
+ println("Cancelling the supervisor")
supervisor.cancel()
secondChild.join()
}
@@ -391,24 +398,24 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-supervision-01.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-supervision-01.kt).
The output of this code is:
```text
-First child is failing
-First child is cancelled: true, but second one is still active
-Cancelling supervisor
-Second child is cancelled because supervisor is cancelled
+The first child is failing
+The first child is cancelled: true, but the second one is still active
+Cancelling the supervisor
+The second child is cancelled because the supervisor was cancelled
```
#### Supervision scope
-For *scoped* concurrency [supervisorScope] can be used instead of [coroutineScope] for the same purpose. It propagates cancellation
-only in one direction and cancels all children only if it has failed itself. It also waits for all children before completion
-just like [coroutineScope] does.
+Instead of [coroutineScope][_coroutineScope], we can use [supervisorScope][_supervisorScope] for _scoped_ concurrency. It propagates the cancellation
+in one direction only and cancels all its children only if it failed itself. It also waits for all children before completion
+just like [coroutineScope][_coroutineScope] does.
@@ -421,42 +428,45 @@ fun main() = runBlocking {
supervisorScope {
val child = launch {
try {
- println("Child is sleeping")
+ println("The child is sleeping")
delay(Long.MAX_VALUE)
} finally {
- println("Child is cancelled")
+ println("The child is cancelled")
}
}
// Give our child a chance to execute and print using yield
yield()
- println("Throwing exception from scope")
+ println("Throwing an exception from the scope")
throw AssertionError()
}
} catch(e: AssertionError) {
- println("Caught assertion error")
+ println("Caught an assertion error")
}
}
```
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-supervision-02.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-supervision-02.kt).
The output of this code is:
```text
-Child is sleeping
-Throwing exception from scope
-Child is cancelled
-Caught assertion error
+The child is sleeping
+Throwing an exception from the scope
+The child is cancelled
+Caught an assertion error
```
#### Exceptions in supervised coroutines
Another crucial difference between regular and supervisor jobs is exception handling.
-Every child should handle its exceptions by itself via exception handling mechanisms.
-This difference comes from the fact that child's failure is not propagated to the parent.
+Every child should handle its exceptions by itself via the exception handling mechanism.
+This difference comes from the fact that child's failure does not propagate to the parent.
+It means that coroutines launched directly inside the [supervisorScope][_supervisorScope] _do_ use the [CoroutineExceptionHandler]
+that is installed in their scope in the same way as root coroutines do
+(see the [CoroutineExceptionHandler](#coroutineexceptionhandler) section for details).
@@ -466,30 +476,30 @@ import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
- println("Caught $exception")
+ println("CoroutineExceptionHandler got $exception")
}
supervisorScope {
val child = launch(handler) {
- println("Child throws an exception")
+ println("The child throws an exception")
throw AssertionError()
}
- println("Scope is completing")
+ println("The scope is completing")
}
- println("Scope is completed")
+ println("The scope is completed")
}
```
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt).
The output of this code is:
```text
-Scope is completing
-Child throws an exception
-Caught java.lang.AssertionError
-Scope is completed
+The scope is completing
+The child throws an exception
+CoroutineExceptionHandler got java.lang.AssertionError
+The scope is completed
```
@@ -501,12 +511,14 @@ Scope is completed
[Deferred.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html
[GlobalScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html
[CoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html
+[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
+[Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
[Job.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/cancel.html
[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
[SupervisorJob()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html
[Job()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job.html
-[supervisorScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html
-[coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
+[_coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
+[_supervisorScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html
[actor]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html
[produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html
diff --git a/docs/flow.md b/docs/flow.md
index 705f338b20..4374e7aa86 100644
--- a/docs/flow.md
+++ b/docs/flow.md
@@ -10,7 +10,7 @@
* [Suspending functions](#suspending-functions)
* [Flows](#flows)
* [Flows are cold](#flows-are-cold)
- * [Flow cancellation](#flow-cancellation)
+ * [Flow cancellation basics](#flow-cancellation-basics)
* [Flow builders](#flow-builders)
* [Intermediate flow operators](#intermediate-flow-operators)
* [Transform operator](#transform-operator)
@@ -39,31 +39,33 @@
* [Flow completion](#flow-completion)
* [Imperative finally block](#imperative-finally-block)
* [Declarative handling](#declarative-handling)
- * [Upstream exceptions only](#upstream-exceptions-only)
+ * [Successful completion](#successful-completion)
* [Imperative versus declarative](#imperative-versus-declarative)
* [Launching flow](#launching-flow)
+ * [Flow cancellation checks](#flow-cancellation-checks)
+ * [Making busy flow cancellable](#making-busy-flow-cancellable)
* [Flow and Reactive Streams](#flow-and-reactive-streams)
## Asynchronous Flow
-Suspending functions asynchronously returns a single value, but how can we return
+A suspending function asynchronously returns a single value, but how can we return
multiple asynchronously computed values? This is where Kotlin Flows come in.
### Representing multiple values
Multiple values can be represented in Kotlin using [collections].
-For example, we can have a function `foo()` that returns a [List]
+For example, we can have a `simple` function that returns a [List]
of three numbers and then print them all using [forEach]:
```kotlin
-fun foo(): List
= listOf(1, 2, 3)
+fun simple(): List = listOf(1, 2, 3)
fun main() {
- foo().forEach { value -> println(value) }
+ simple().forEach { value -> println(value) }
}
```
@@ -89,7 +91,7 @@ If we are computing the numbers with some CPU-consuming blocking code
```kotlin
-fun foo(): Sequence
= sequence { // sequence builder
+fun simple(): Sequence = sequence { // sequence builder
for (i in 1..3) {
Thread.sleep(100) // pretend we are computing it
yield(i) // yield next value
@@ -97,7 +99,7 @@ fun foo(): Sequence = sequence { // sequence builder
}
fun main() {
- foo().forEach { value -> println(value) }
+ simple().forEach { value -> println(value) }
}
```
@@ -116,7 +118,7 @@ This code outputs the same numbers, but it waits 100ms before printing each one.
#### Suspending functions
However, this computation blocks the main thread that is running the code.
-When these values are computed by asynchronous code we can mark the function `foo` with a `suspend` modifier,
+When these values are computed by asynchronous code we can mark the `simple` function with a `suspend` modifier,
so that it can perform its work without blocking and return the result as a list:
@@ -125,13 +127,13 @@ so that it can perform its work without blocking and return the result as a list
import kotlinx.coroutines.*
//sampleStart
-suspend fun foo(): List
{
+suspend fun simple(): List {
delay(1000) // pretend we are doing something asynchronous here
return listOf(1, 2, 3)
}
fun main() = runBlocking {
- foo().forEach { value -> println(value) }
+ simple().forEach { value -> println(value) }
}
//sampleEnd
```
@@ -151,7 +153,7 @@ This code prints the numbers after waiting for a second.
#### Flows
Using the `List` result type, means we can only return all the values at once. To represent
-the stream of values that are being asynchronously computed, we can use a [`Flow`][Flow] type just like we would the `Sequence` type for synchronously computed values:
+the stream of values that are being asynchronously computed, we can use a [`Flow`][Flow] type just like we would use the `Sequence` type for synchronously computed values:
@@ -160,7 +162,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
-fun foo(): Flow
= flow { // flow builder
+fun simple(): Flow = flow { // flow builder
for (i in 1..3) {
delay(100) // pretend we are doing something useful here
emit(i) // emit next value
@@ -176,7 +178,7 @@ fun main() = runBlocking {
}
}
// Collect the flow
- foo().collect { value -> println(value) }
+ simple().collect { value -> println(value) }
}
//sampleEnd
```
@@ -201,18 +203,18 @@ I'm not blocked 3
Notice the following differences in the code with the [Flow] from the earlier examples:
-* A builder function for [Flow] type is called [flow].
+* A builder function for [Flow] type is called [flow][_flow].
* Code inside the `flow { ... }` builder block can suspend.
-* The function `foo()` is no longer marked with `suspend` modifier.
+* The `simple` function is no longer marked with `suspend` modifier.
* Values are _emitted_ from the flow using [emit][FlowCollector.emit] function.
* Values are _collected_ from the flow using [collect][collect] function.
-> We can replace [delay] with `Thread.sleep` in the body of `foo`'s `flow { ... }` and see that the main
+> We can replace [delay] with `Thread.sleep` in the body of `simple`'s `flow { ... }` and see that the main
thread is blocked in this case.
### Flows are cold
-Flows are _cold_ streams similar to sequences — the code inside a [flow] builder does not
+Flows are _cold_ streams similar to sequences — the code inside a [flow][_flow] builder does not
run until the flow is collected. This becomes clear in the following example:
@@ -222,7 +224,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
-fun foo(): Flow
= flow {
+fun simple(): Flow = flow {
println("Flow started")
for (i in 1..3) {
delay(100)
@@ -231,8 +233,8 @@ fun foo(): Flow = flow {
}
fun main() = runBlocking {
- println("Calling foo...")
- val flow = foo()
+ println("Calling simple function...")
+ val flow = simple()
println("Calling collect...")
flow.collect { value -> println(value) }
println("Calling collect again...")
@@ -248,7 +250,7 @@ fun main() = runBlocking {
Which prints:
```text
-Calling foo...
+Calling simple function...
Calling collect...
Flow started
1
@@ -263,16 +265,14 @@ Flow started
-This is a key reason the `foo()` function (which returns a flow) is not marked with `suspend` modifier.
-By itself, `foo()` returns quickly and does not wait for anything. The flow starts every time it is collected,
+This is a key reason the `simple` function (which returns a flow) is not marked with `suspend` modifier.
+By itself, `simple()` call returns quickly and does not wait for anything. The flow starts every time it is collected,
that is why we see "Flow started" when we call `collect` again.
-### Flow cancellation
-
-Flow adheres to the general cooperative cancellation of coroutines. However, flow infrastructure does not introduce
-additional cancellation points. It is fully transparent for cancellation. As usual, flow collection can be
-cancelled when the flow is suspended in a cancellable suspending function (like [delay]), and cannot be cancelled otherwise.
+### Flow cancellation basics
+Flow adheres to the general cooperative cancellation of coroutines. As usual, flow collection can be
+cancelled when the flow is suspended in a cancellable suspending function (like [delay]).
The following example shows how the flow gets cancelled on a timeout when running in a [withTimeoutOrNull] block
and stops executing its code:
@@ -283,7 +283,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
-fun foo(): Flow = flow {
+fun simple(): Flow = flow {
for (i in 1..3) {
delay(100)
println("Emitting $i")
@@ -293,7 +293,7 @@ fun foo(): Flow = flow {
fun main() = runBlocking {
withTimeoutOrNull(250) { // Timeout after 250ms
- foo().collect { value -> println(value) }
+ simple().collect { value -> println(value) }
}
println("Done")
}
@@ -304,7 +304,7 @@ fun main() = runBlocking {
> You can get the full code from [here](../kotlinx-coroutines-core/jvm/test/guide/example-flow-06.kt).
-Notice how only two numbers get emitted by the flow in `foo()` function, producing the following output:
+Notice how only two numbers get emitted by the flow in the `simple` function, producing the following output:
```text
Emitting 1
@@ -316,6 +316,8 @@ Done
+See [Flow cancellation checks](#flow-cancellation-checks) section for more details.
+
### Flow builders
The `flow { ... }` builder from the previous examples is the most basic one. There are other builders for
@@ -590,14 +592,14 @@ Filter 5
### Flow context
Collection of a flow always happens in the context of the calling coroutine. For example, if there is
-a `foo` flow, then the following code runs in the context specified
-by the author of this code, regardless of the implementation details of the `foo` flow:
+a `simple` flow, then the following code runs in the context specified
+by the author of this code, regardless of the implementation details of the `simple` flow:
```kotlin
withContext(context) {
- foo.collect { value ->
+ simple().collect { value ->
println(value) // run in the specified context
}
}
@@ -610,7 +612,7 @@ withContext(context) {
This property of a flow is called _context preservation_.
So, by default, code in the `flow { ... }` builder runs in the context that is provided by a collector
-of the corresponding flow. For example, consider the implementation of `foo` that prints the thread
+of the corresponding flow. For example, consider the implementation of a `simple` function that prints the thread
it is called on and emits three numbers:
@@ -622,15 +624,15 @@ import kotlinx.coroutines.flow.*
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
//sampleStart
-fun foo(): Flow
= flow {
- log("Started foo flow")
+fun simple(): Flow = flow {
+ log("Started simple flow")
for (i in 1..3) {
emit(i)
}
}
fun main() = runBlocking {
- foo().collect { value -> log("Collected $value") }
+ simple().collect { value -> log("Collected $value") }
}
//sampleEnd
```
@@ -642,7 +644,7 @@ fun main() = runBlocking {
Running this code produces:
```text
-[main @coroutine#1] Started foo flow
+[main @coroutine#1] Started simple flow
[main @coroutine#1] Collected 1
[main @coroutine#1] Collected 2
[main @coroutine#1] Collected 3
@@ -650,7 +652,7 @@ Running this code produces:
-Since `foo().collect` is called from the main thread, the body of `foo`'s flow is also called in the main thread.
+Since `simple().collect` is called from the main thread, the body of `simple`'s flow is also called in the main thread.
This is the perfect default for fast-running or asynchronous code that does not care about the execution context and
does not block the caller.
@@ -670,7 +672,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
-fun foo(): Flow = flow {
+fun simple(): Flow = flow {
// The WRONG way to change context for CPU-consuming code in flow builder
kotlinx.coroutines.withContext(Dispatchers.Default) {
for (i in 1..3) {
@@ -681,7 +683,7 @@ fun foo(): Flow = flow {
}
fun main() = runBlocking {
- foo().collect { value -> println(value) }
+ simple().collect { value -> println(value) }
}
//sampleEnd
```
@@ -695,7 +697,7 @@ This code produces the following exception:
```text
Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
Flow was collected in [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@5511c7f8, BlockingEventLoop@2eac3323],
- but emission happened in [CoroutineId(1), "coroutine#1":DispatchedCoroutine{Active}@2dae0000, DefaultDispatcher].
+ but emission happened in [CoroutineId(1), "coroutine#1":DispatchedCoroutine{Active}@2dae0000, Dispatchers.Default].
Please refer to 'flow' documentation or use 'flowOn' instead
at ...
```
@@ -717,7 +719,7 @@ import kotlinx.coroutines.flow.*
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
//sampleStart
-fun foo(): Flow = flow {
+fun simple(): Flow = flow {
for (i in 1..3) {
Thread.sleep(100) // pretend we are computing it in CPU-consuming way
log("Emitting $i")
@@ -726,7 +728,7 @@ fun foo(): Flow = flow {
}.flowOn(Dispatchers.Default) // RIGHT way to change context for CPU-consuming code in flow builder
fun main() = runBlocking {
- foo().collect { value ->
+ simple().collect { value ->
log("Collected $value")
}
}
@@ -757,7 +759,7 @@ creates another coroutine for an upstream flow when it has to change the [Corout
Running different parts of a flow in different coroutines can be helpful from the standpoint of the overall time it takes
to collect the flow, especially when long-running asynchronous operations are involved. For example, consider a case when
-the emission by `foo()` flow is slow, taking 100 ms to produce an element; and collector is also slow,
+the emission by a `simple` flow is slow, taking 100 ms to produce an element; and collector is also slow,
taking 300 ms to process an element. Let's see how long it takes to collect such a flow with three numbers:
@@ -768,7 +770,7 @@ import kotlinx.coroutines.flow.*
import kotlin.system.*
//sampleStart
-fun foo(): Flow
= flow {
+fun simple(): Flow = flow {
for (i in 1..3) {
delay(100) // pretend we are asynchronously waiting 100 ms
emit(i) // emit next value
@@ -777,7 +779,7 @@ fun foo(): Flow = flow {
fun main() = runBlocking {
val time = measureTimeMillis {
- foo().collect { value ->
+ simple().collect { value ->
delay(300) // pretend we are processing it for 300 ms
println(value)
}
@@ -802,7 +804,7 @@ Collected in 1220 ms
-We can use a [buffer] operator on a flow to run emitting code of `foo()` concurrently with collecting code,
+We can use a [buffer] operator on a flow to run emitting code of the `simple` flow concurrently with collecting code,
as opposed to running them sequentially:
@@ -812,7 +814,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.*
-fun foo(): Flow
= flow {
+fun simple(): Flow = flow {
for (i in 1..3) {
delay(100) // pretend we are asynchronously waiting 100 ms
emit(i) // emit next value
@@ -822,7 +824,7 @@ fun foo(): Flow = flow {
fun main() = runBlocking {
//sampleStart
val time = measureTimeMillis {
- foo()
+ simple()
.buffer() // buffer emissions, don't wait
.collect { value ->
delay(300) // pretend we are processing it for 300 ms
@@ -867,7 +869,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.*
-fun foo(): Flow = flow {
+fun simple(): Flow = flow {
for (i in 1..3) {
delay(100) // pretend we are asynchronously waiting 100 ms
emit(i) // emit next value
@@ -877,7 +879,7 @@ fun foo(): Flow = flow {
fun main() = runBlocking {
//sampleStart
val time = measureTimeMillis {
- foo()
+ simple()
.conflate() // conflate emissions, don't process each one
.collect { value ->
delay(300) // pretend we are processing it for 300 ms
@@ -918,7 +920,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.*
-fun foo(): Flow = flow {
+fun simple(): Flow = flow {
for (i in 1..3) {
delay(100) // pretend we are asynchronously waiting 100 ms
emit(i) // emit next value
@@ -928,7 +930,7 @@ fun foo(): Flow = flow {
fun main() = runBlocking {
//sampleStart
val time = measureTimeMillis {
- foo()
+ simple()
.collectLatest { value -> // cancel & restart on the latest value
println("Collecting $value")
delay(300) // pretend we are processing it for 300 ms
@@ -1110,7 +1112,7 @@ Now if we have a flow of three integers and call `requestFlow` for each of them
Then we end up with a flow of flows (`Flow>`) that needs to be _flattened_ into a single flow for
further processing. Collections and sequences have [flatten][Sequence.flatten] and [flatMap][Sequence.flatMap]
-operators for this. However, due the asynchronous nature of flows they call for different _modes_ of flattening,
+operators for this. However, due to the asynchronous nature of flows they call for different _modes_ of flattening,
as such, there is a family of flattening operators on flows.
#### flatMapConcat
@@ -1279,7 +1281,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
-fun foo(): Flow = flow {
+fun simple(): Flow = flow {
for (i in 1..3) {
println("Emitting $i")
emit(i) // emit next value
@@ -1288,7 +1290,7 @@ fun foo(): Flow = flow {
fun main() = runBlocking {
try {
- foo().collect { value ->
+ simple().collect { value ->
println(value)
check(value <= 1) { "Collected $value" }
}
@@ -1329,7 +1331,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
-fun foo(): Flow =
+fun simple(): Flow =
flow {
for (i in 1..3) {
println("Emitting $i")
@@ -1343,7 +1345,7 @@ fun foo(): Flow =
fun main() = runBlocking {
try {
- foo().collect { value -> println(value) }
+ simple().collect { value -> println(value) }
} catch (e: Throwable) {
println("Caught $e")
}
@@ -1390,7 +1392,7 @@ For example, let us emit the text on catching an exception:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
-fun foo(): Flow =
+fun simple(): Flow =
flow {
for (i in 1..3) {
println("Emitting $i")
@@ -1404,7 +1406,7 @@ fun foo(): Flow =
fun main() = runBlocking {
//sampleStart
- foo()
+ simple()
.catch { e -> emit("Caught $e") } // emit on exception
.collect { value -> println(value) }
//sampleEnd
@@ -1437,7 +1439,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
-fun foo(): Flow = flow {
+fun simple(): Flow = flow {
for (i in 1..3) {
println("Emitting $i")
emit(i)
@@ -1445,7 +1447,7 @@ fun foo(): Flow = flow {
}
fun main() = runBlocking {
- foo()
+ simple()
.catch { e -> println("Caught $e") } // does not catch downstream exceptions
.collect { value ->
check(value <= 1) { "Collected $value" }
@@ -1461,13 +1463,15 @@ fun main() = runBlocking {
A "Caught ..." message is not printed despite there being a `catch` operator:
-
+```
+
+
#### Catching declaratively
@@ -1481,7 +1485,7 @@ be triggered by a call to `collect()` without parameters:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
-fun foo(): Flow = flow {
+fun simple(): Flow = flow {
for (i in 1..3) {
println("Emitting $i")
emit(i)
@@ -1490,7 +1494,7 @@ fun foo(): Flow = flow {
fun main() = runBlocking {
//sampleStart
- foo()
+ simple()
.onEach { value ->
check(value <= 1) { "Collected $value" }
println(value)
@@ -1508,12 +1512,14 @@ fun main() = runBlocking {
Now we can see that a "Caught ..." message is printed and so we can catch all the exceptions without explicitly
using a `try/catch` block:
-
+```
+
+
### Flow completion
@@ -1532,11 +1538,11 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
-fun foo(): Flow = (1..3).asFlow()
+fun simple(): Flow = (1..3).asFlow()
fun main() = runBlocking {
try {
- foo().collect { value -> println(value) }
+ simple().collect { value -> println(value) }
} finally {
println("Done")
}
@@ -1548,7 +1554,7 @@ fun main() = runBlocking {
> You can get the full code from [here](../kotlinx-coroutines-core/jvm/test/guide/example-flow-31.kt).
-This code prints three numbers produced by the `foo()` flow followed by a "Done" string:
+This code prints three numbers produced by the `simple` flow followed by a "Done" string:
```text
1
@@ -1572,11 +1578,11 @@ The previous example can be rewritten using an [onCompletion] operator and produ
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
-fun foo(): Flow = (1..3).asFlow()
+fun simple(): Flow = (1..3).asFlow()
fun main() = runBlocking {
//sampleStart
- foo()
+ simple()
.onCompletion { println("Done") }
.collect { value -> println(value) }
//sampleEnd
@@ -1595,7 +1601,7 @@ Done
The key advantage of [onCompletion] is a nullable `Throwable` parameter of the lambda that can be used
to determine whether the flow collection was completed normally or exceptionally. In the following
-example the `foo()` flow throws an exception after emitting the number 1:
+example the `simple` flow throws an exception after emitting the number 1:
@@ -1604,13 +1610,13 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
-fun foo(): Flow
= flow {
+fun simple(): Flow = flow {
emit(1)
throw RuntimeException()
}
fun main() = runBlocking {
- foo()
+ simple()
.onCompletion { cause -> if (cause != null) println("Flow completed exceptionally") }
.catch { cause -> println("Caught exception") }
.collect { value -> println(value) }
@@ -1635,10 +1641,10 @@ The [onCompletion] operator, unlike [catch], does not handle the exception. As w
example code, the exception still flows downstream. It will be delivered to further `onCompletion` operators
and can be handled with a `catch` operator.
-#### Upstream exceptions only
+#### Successful completion
-Just like the [catch] operator, [onCompletion] only sees exceptions coming from upstream and does not
-see downstream exceptions. For example, run the following code:
+Another difference with [catch] operator is that [onCompletion] sees all exceptions and receives
+a `null` exception only on successful completion of the upstream flow (without cancellation or failure).
@@ -1647,10 +1653,10 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
-fun foo(): Flow
= (1..3).asFlow()
+fun simple(): Flow = (1..3).asFlow()
fun main() = runBlocking {
- foo()
+ simple()
.onCompletion { cause -> println("Flow completed with $cause") }
.collect { value ->
check(value <= 1) { "Collected $value" }
@@ -1664,11 +1670,11 @@ fun main() = runBlocking {
> You can get the full code from [here](../kotlinx-coroutines-core/jvm/test/guide/example-flow-34.kt).
-We can see the completion cause is null, yet collection failed with exception:
+We can see the completion cause is not null, because the flow was aborted due to downstream exception:
```text
1
-Flow completed with null
+Flow completed with java.lang.IllegalStateException: Collected 2
Exception in thread "main" java.lang.IllegalStateException: Collected 2
```
@@ -1777,6 +1783,127 @@ as cancellation and structured concurrency serve this purpose.
Note that [launchIn] also returns a [Job], which can be used to [cancel][Job.cancel] the corresponding flow collection
coroutine only without cancelling the whole scope or to [join][Job.join] it.
+### Flow cancellation checks
+
+For convenience, the [flow][_flow] builder performs additional [ensureActive] checks for cancellation on each emitted value.
+It means that a busy loop emitting from a `flow { ... }` is cancellable:
+
+
+
+```kotlin
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.*
+
+//sampleStart
+fun foo(): Flow = flow {
+ for (i in 1..5) {
+ println("Emitting $i")
+ emit(i)
+ }
+}
+
+fun main() = runBlocking {
+ foo().collect { value ->
+ if (value == 3) cancel()
+ println(value)
+ }
+}
+//sampleEnd
+```
+
+
+
+> You can get the full code from [here](../kotlinx-coroutines-core/jvm/test/guide/example-flow-37.kt).
+
+We get only numbers up to 3 and a [CancellationException] after trying to emit number 4:
+
+```text
+Emitting 1
+1
+Emitting 2
+2
+Emitting 3
+3
+Emitting 4
+Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@6d7b4f4c
+```
+
+
+
+However, most other flow operators do not do additional cancellation checks on their own for performance reasons.
+For example, if you use [IntRange.asFlow] extension to write the same busy loop and don't suspend anywhere,
+then there are no checks for cancellation:
+
+
+
+```kotlin
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.*
+
+//sampleStart
+fun main() = runBlocking {
+ (1..5).asFlow().collect { value ->
+ if (value == 3) cancel()
+ println(value)
+ }
+}
+//sampleEnd
+```
+
+
+
+> You can get the full code from [here](../kotlinx-coroutines-core/jvm/test/guide/example-flow-38.kt).
+
+All numbers from 1 to 5 are collected and cancellation gets detected only before return from `runBlocking`:
+
+```text
+1
+2
+3
+4
+5
+Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@3327bd23
+```
+
+
+
+#### Making busy flow cancellable
+
+In the case where you have a busy loop with coroutines you must explicitly check for cancellation.
+You can add `.onEach { currentCoroutineContext().ensureActive() }`, but there is a ready-to-use
+[cancellable] operator provided to do that:
+
+
+
+```kotlin
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.*
+
+//sampleStart
+fun main() = runBlocking {
+ (1..5).asFlow().cancellable().collect { value ->
+ if (value == 3) cancel()
+ println(value)
+ }
+}
+//sampleEnd
+```
+
+
+
+> You can get the full code from [here](../kotlinx-coroutines-core/jvm/test/guide/example-flow-39.kt).
+
+With the `cancellable` operator only the numbers from 1 to 3 are collected:
+
+```text
+1
+2
+3
+Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@5ec0a365
+```
+
+
+
### Flow and Reactive Streams
For those who are familiar with [Reactive Streams](https://www.reactive-streams.org/) or reactive frameworks such as RxJava and project Reactor,
@@ -1786,7 +1913,7 @@ Indeed, its design was inspired by Reactive Streams and its various implementati
be Kotlin and suspension friendly and respect structured concurrency. Achieving this goal would be impossible without reactive pioneers and their tremendous work. You can read the complete story in [Reactive Streams and Kotlin Flows](https://medium.com/@elizarov/reactive-streams-and-kotlin-flows-bfd12772cda4) article.
While being different, conceptually, Flow *is* a reactive stream and it is possible to convert it to the reactive (spec and TCK compliant) Publisher and vice versa.
-Such converters are provided by `kotlinx.coroutines` out-of-the-box and can be found in corresponding reactive modules (`kotlinx-coroutines-reactive` for Reactive Streams, `kotlinx-coroutines-reactor` for Project Reactor and `kotlinx-coroutines-rx2` for RxJava2).
+Such converters are provided by `kotlinx.coroutines` out-of-the-box and can be found in corresponding reactive modules (`kotlinx-coroutines-reactive` for Reactive Streams, `kotlinx-coroutines-reactor` for Project Reactor and `kotlinx-coroutines-rx2`/`kotlinx-coroutines-rx3` for RxJava2/RxJava3).
Integration modules include conversions from and to `Flow`, integration with Reactor's `Context` and suspension-friendly ways to work with various reactive entities.
@@ -1813,9 +1940,11 @@ Integration modules include conversions from and to `Flow`, integration with Rea
[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
[Job.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/cancel.html
[Job.join]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html
+[ensureActive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/ensure-active.html
+[CancellationException]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html
[Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
-[flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html
+[_flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html
[FlowCollector.emit]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow-collector/emit.html
[collect]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/collect.html
[flowOf]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow-of.html
@@ -1845,4 +1974,6 @@ Integration modules include conversions from and to `Flow`, integration with Rea
[catch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/catch.html
[onCompletion]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/on-completion.html
[launchIn]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/launch-in.html
+[IntRange.asFlow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/kotlin.ranges.-int-range/as-flow.html
+[cancellable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/cancellable.html
diff --git a/docs/images/coroutine-idea-debugging-1.png b/docs/images/coroutine-idea-debugging-1.png
new file mode 100644
index 0000000000..0afe992515
Binary files /dev/null and b/docs/images/coroutine-idea-debugging-1.png differ
diff --git a/docs/knit.properties b/docs/knit.properties
index ab2508a114..2028ecb416 100644
--- a/docs/knit.properties
+++ b/docs/knit.properties
@@ -4,19 +4,7 @@
knit.package=kotlinx.coroutines.guide
knit.dir=../kotlinx-coroutines-core/jvm/test/guide/
-knit.pattern=example-[a-zA-Z0-9-]+-##\\.kt
-knit.include=knit.code.include
test.package=kotlinx.coroutines.guide.test
test.dir=../kotlinx-coroutines-core/jvm/test/guide/test/
-test.template=knit.test.template
-# Various test validation modes and their corresponding methods from TestUtil
-test.mode.=verifyLines
-test.mode.STARTS_WITH=verifyLinesStartWith
-test.mode.ARBITRARY_TIME=verifyLinesArbitraryTime
-test.mode.FLEXIBLE_TIME=verifyLinesFlexibleTime
-test.mode.FLEXIBLE_THREAD=verifyLinesFlexibleThread
-test.mode.LINES_START_UNORDERED=verifyLinesStartUnordered
-test.mode.LINES_START=verifyLinesStart
-test.mode.EXCEPTION=verifyExceptions
\ No newline at end of file
diff --git a/docs/knit.test.template b/docs/knit.test.template
index a912555a43..727493c662 100644
--- a/docs/knit.test.template
+++ b/docs/knit.test.template
@@ -5,6 +5,7 @@
// This file was automatically generated from ${file.name} by Knit tool. Do not edit.
package ${test.package}
+import kotlinx.coroutines.knit.*
import org.junit.Test
class ${test.name} {
diff --git a/docs/select-expression.md b/docs/select-expression.md
index 5809e7b93e..f0e5ae4681 100644
--- a/docs/select-expression.md
+++ b/docs/select-expression.md
@@ -125,7 +125,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-01.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-01.kt).
The result of this code is:
@@ -220,7 +220,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-02.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-02.kt).
The result of this code is quite interesting, so we'll analyze it in mode detail:
@@ -310,7 +310,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-03.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-03.kt).
So let us see what happens:
@@ -403,7 +403,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-04.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-04.kt).
The output is:
@@ -522,7 +522,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-05.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-05.kt).
The result of this code:
diff --git a/docs/shared-mutable-state-and-concurrency.md b/docs/shared-mutable-state-and-concurrency.md
index 1a3c406472..8b83ad0b20 100644
--- a/docs/shared-mutable-state-and-concurrency.md
+++ b/docs/shared-mutable-state-and-concurrency.md
@@ -24,7 +24,7 @@ but others are unique.
### The problem
-Let us launch a hundred coroutines all doing the same action thousand times.
+Let us launch a hundred coroutines all doing the same action a thousand times.
We'll also measure their completion time for further comparisons:
@@ -90,7 +90,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-sync-01.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-sync-01.kt).
@@ -144,7 +144,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-sync-02.kt).
+> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-sync-02.kt).
concurrent ---> common
+ ^
+ ios \ |
+ macos | ---> nativeDarwin ---> native --+
+ tvos | ^
+ watchos / |
+ |
+ linux \ ---> nativeOther -------+
+ mingw /
+
+ ========================================================================== */
+
+project.ext.sourceSetSuffixes = ["Main", "Test"]
+
+void defineSourceSet(newName, dependsOn, includedInPred) {
+ for (suffix in project.ext.sourceSetSuffixes) {
+ def newSS = kotlin.sourceSets.maybeCreate(newName + suffix)
+ for (dep in dependsOn) {
+ newSS.dependsOn(kotlin.sourceSets[dep + suffix])
+ }
+ for (curSS in kotlin.sourceSets) {
+ def curName = curSS.name
+ if (curName.endsWith(suffix)) {
+ def prefix = curName.substring(0, curName.length() - suffix.length())
+ if (includedInPred(prefix)) curSS.dependsOn(newSS)
+ }
+ }
+ }
+}
+
+static boolean isNativeDarwin(String name) { return ["ios", "macos", "tvos", "watchos"].any { name.startsWith(it) } }
+static boolean isNativeOther(String name) { return ["linux", "mingw"].any { name.startsWith(it) } }
+
+defineSourceSet("concurrent", ["common"]) { it in ["jvm", "native"] }
+defineSourceSet("nativeDarwin", ["native"]) { isNativeDarwin(it) }
+defineSourceSet("nativeOther", ["native"]) { isNativeOther(it) }
+
+/* ========================================================================== */
+
/*
* All platform plugins and configuration magic happens here instead of build.gradle
* because JMV-only projects depend on core, thus core should always be initialized before configuration.
@@ -18,7 +65,7 @@ kotlin {
configure(sourceSets) {
def srcDir = name.endsWith('Main') ? 'src' : 'test'
def platform = name[0..-5]
- kotlin.srcDir "$platform/$srcDir"
+ kotlin.srcDirs = ["$platform/$srcDir"]
if (name == "jvmMain") {
resources.srcDirs = ["$platform/resources"]
} else if (name == "jvmTest") {
@@ -31,12 +78,18 @@ kotlin {
}
configure(targets) {
- def targetName = it.name
- compilations.all { compilation ->
- def compileTask = tasks.getByName(compilation.compileKotlinTaskName)
- // binary compatibility support
- if (targetName.contains("jvm") && compilation.compilationName == "main") {
- compileTask.kotlinOptions.freeCompilerArgs += ["-Xdump-declarations-to=${buildDir}/visibilities.json"]
+ // Configure additional binaries and test runs -- one for each OS
+ if (["macos", "linux", "mingw"].any { name.startsWith(it) }) {
+ binaries {
+ // Test for memory leaks using a special entry point that does not exit but returns from main
+ binaries.getTest("DEBUG").freeCompilerArgs += ["-e", "kotlinx.coroutines.mainNoExit"]
+ // Configure a separate test where code runs in background
+ test("background", [org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG]) {
+ freeCompilerArgs += ["-e", "kotlinx.coroutines.mainBackground"]
+ }
+ }
+ testRuns {
+ background { setExecutionSourceFrom(binaries.backgroundDebugTest) }
}
}
}
@@ -54,12 +107,52 @@ compileKotlinMetadata {
}
}
+// :KLUDGE: Idea.active: This is needed to workaround resolve problems after importing this project to IDEA
+def configureNativeSourceSetPreset(name, preset) {
+ def hostMainCompilation = project.kotlin.targetFromPreset(preset).compilations.main
+ // Look for platform libraries in "implementation" for default source set
+ def implementationConfiguration = configurations[hostMainCompilation.defaultSourceSet.implementationMetadataConfigurationName]
+ // Now find the libraries: Finds platform libs & stdlib, but platform declarations are still not resolved due to IDE bugs
+ def hostNativePlatformLibs = files(
+ provider {
+ implementationConfiguration.findAll {
+ it.path.endsWith(".klib") || it.absolutePath.contains("klib${File.separator}platform") || it.absolutePath.contains("stdlib")
+ }
+ }
+ )
+ // Add all those dependencies
+ for (suffix in sourceSetSuffixes) {
+ configure(kotlin.sourceSets[name + suffix]) {
+ dependencies.add(implementationMetadataConfigurationName, hostNativePlatformLibs)
+ }
+ }
+}
+
+// :KLUDGE: Idea.active: Configure platform libraries for native source sets when working in IDEA
+if (Idea.active) {
+ def manager = project.ext.hostManager
+ def linuxPreset = kotlin.presets.linuxX64
+ def macosPreset = kotlin.presets.macosX64
+ // linux should be always available (cross-compilation capable) -- use it as default
+ assert manager.isEnabled(linuxPreset.konanTarget)
+ // use macOS libs for nativeDarwin if available
+ def macosAvailable = manager.isEnabled(macosPreset.konanTarget)
+ // configure source sets
+ configureNativeSourceSetPreset("native", linuxPreset)
+ configureNativeSourceSetPreset("nativeOther", linuxPreset)
+ configureNativeSourceSetPreset("nativeDarwin", macosAvailable ? macosPreset : linuxPreset)
+}
+
kotlin.sourceSets {
+ jvmMain.dependencies {
+ compileOnly "com.google.android:annotations:4.1.1.4"
+ }
+
jvmTest.dependencies {
api "org.jetbrains.kotlinx:lincheck:$lincheck_version"
api "org.jetbrains.kotlinx:kotlinx-knit-test:$knit_version"
api "com.esotericsoftware:kryo:4.0.0"
- implementation project (":android-unit-tests")
+ implementation project(":android-unit-tests")
}
}
@@ -86,12 +179,19 @@ jvmTest {
enableAssertions = true
systemProperty 'java.security.manager', 'kotlinx.coroutines.TestSecurityManager'
// 'stress' is required to be able to run all subpackage tests like ":jvmTests --tests "*channels*" -Pstress=true"
- if (!project.ext.ideaActive && rootProject.properties['stress'] == null) {
+ if (!Idea.active && rootProject.properties['stress'] == null) {
exclude '**/*StressTest.*'
}
systemProperty 'kotlinx.coroutines.scheduler.keep.alive.sec', '100000' // any unpark problem hangs test
}
+jvmJar {
+ manifest {
+ attributes "Premain-Class": "kotlinx.coroutines.debug.AgentPremain"
+ attributes "Can-Retransform-Classes": "true"
+ }
+}
+
task jvmStressTest(type: Test, dependsOn: compileTestKotlinJvm) {
classpath = files { jvmTest.classpath }
testClassesDirs = files { jvmTest.testClassesDirs }
@@ -101,6 +201,8 @@ task jvmStressTest(type: Test, dependsOn: compileTestKotlinJvm) {
enableAssertions = true
testLogging.showStandardStreams = true
systemProperty 'kotlinx.coroutines.scheduler.keep.alive.sec', '100000' // any unpark problem hangs test
+ systemProperty 'kotlinx.coroutines.semaphore.segmentSize', '2'
+ systemProperty 'kotlinx.coroutines.semaphore.maxSpinCycles', '10'
}
task jdk16Test(type: Test, dependsOn: [compileTestKotlinJvm, checkJdk16]) {
@@ -111,6 +213,7 @@ task jdk16Test(type: Test, dependsOn: [compileTestKotlinJvm, checkJdk16]) {
exclude '**/*LCStressTest.*' // lin-check tests use LinChecker which needs JDK8
exclude '**/exceptions/**' // exceptions tests check suppressed exception which needs JDK8
exclude '**/ExceptionsGuideTest.*'
+ exclude '**/RunInterruptibleStressTest.*' // fails on JDK 1.6 due to JDK bug
}
// Run these tests only during nightly stress test
diff --git a/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt b/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt
index 22111f0ce2..742c9670b7 100644
--- a/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt
+++ b/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt
@@ -113,7 +113,7 @@ public abstract class AbstractCoroutine(
afterResume(state)
}
- protected open fun afterResume(state: Any?) = afterCompletion(state)
+ protected open fun afterResume(state: Any?): Unit = afterCompletion(state)
internal final override fun handleOnCompletionException(exception: Throwable) {
handleCoroutineException(context, exception)
diff --git a/kotlinx-coroutines-core/common/src/Await.kt b/kotlinx-coroutines-core/common/src/Await.kt
index dd1e1771f2..7189349024 100644
--- a/kotlinx-coroutines-core/common/src/Await.kt
+++ b/kotlinx-coroutines-core/common/src/Await.kt
@@ -5,6 +5,7 @@
package kotlinx.coroutines
import kotlinx.atomicfu.*
+import kotlinx.coroutines.channels.*
import kotlin.coroutines.*
/**
@@ -18,6 +19,8 @@ import kotlin.coroutines.*
* This suspending function is cancellable.
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting,
* this function immediately resumes with [CancellationException].
+ * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was
+ * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details.
*/
public suspend fun awaitAll(vararg deferreds: Deferred): List =
if (deferreds.isEmpty()) emptyList() else AwaitAll(deferreds).await()
@@ -33,6 +36,8 @@ public suspend fun awaitAll(vararg deferreds: Deferred): List =
* This suspending function is cancellable.
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting,
* this function immediately resumes with [CancellationException].
+ * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was
+ * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details.
*/
public suspend fun Collection>.awaitAll(): List =
if (isEmpty()) emptyList() else AwaitAll(toTypedArray()).await()
@@ -41,8 +46,11 @@ public suspend fun Collection>.awaitAll(): List =
* Suspends current coroutine until all given jobs are complete.
* This method is semantically equivalent to joining all given jobs one by one with `jobs.forEach { it.join() }`.
*
- * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting,
+ * This suspending function is cancellable.
+ * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting,
* this function immediately resumes with [CancellationException].
+ * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was
+ * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details.
*/
public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() }
@@ -50,8 +58,11 @@ public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() }
* Suspends current coroutine until all given jobs are complete.
* This method is semantically equivalent to joining all given jobs one by one with `forEach { it.join() }`.
*
- * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting,
+ * This suspending function is cancellable.
+ * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting,
* this function immediately resumes with [CancellationException].
+ * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was
+ * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details.
*/
public suspend fun Collection.joinAll(): Unit = forEach { it.join() }
diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt
index 7dd1b174ee..b7deaccb72 100644
--- a/kotlinx-coroutines-core/common/src/Builders.common.kt
+++ b/kotlinx-coroutines-core/common/src/Builders.common.kt
@@ -4,6 +4,7 @@
@file:JvmMultifileClass
@file:JvmName("BuildersKt")
+@file:OptIn(ExperimentalContracts::class)
package kotlinx.coroutines
@@ -11,6 +12,7 @@ import kotlinx.atomicfu.*
import kotlinx.coroutines.internal.*
import kotlinx.coroutines.intrinsics.*
import kotlinx.coroutines.selects.*
+import kotlin.contracts.*
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*
import kotlin.jvm.*
@@ -127,38 +129,44 @@ private class LazyDeferredCoroutine(
* This function uses dispatcher from the new context, shifting execution of the [block] into the
* different thread if a new dispatcher is specified, and back to the original dispatcher
* when it completes. Note that the result of `withContext` invocation is
- * dispatched into the original context in a cancellable way, which means that if the original [coroutineContext],
- * in which `withContext` was invoked, is cancelled by the time its dispatcher starts to execute the code,
+ * dispatched into the original context in a cancellable way with a **prompt cancellation guarantee**,
+ * which means that if the original [coroutineContext], in which `withContext` was invoked,
+ * is cancelled by the time its dispatcher starts to execute the code,
* it discards the result of `withContext` and throws [CancellationException].
*/
public suspend fun withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
-): T = suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
- // compute new context
- val oldContext = uCont.context
- val newContext = oldContext + context
- // always check for cancellation of new context
- newContext.checkCompletion()
- // FAST PATH #1 -- new context is the same as the old one
- if (newContext === oldContext) {
- val coroutine = ScopeCoroutine(newContext, uCont)
- return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
+): T {
+ contract {
+ callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
- // FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed)
- // `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher)
- if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
- val coroutine = UndispatchedCoroutine(newContext, uCont)
- // There are changes in the context, so this thread needs to be updated
- withCoroutineContext(newContext, null) {
+ return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
+ // compute new context
+ val oldContext = uCont.context
+ val newContext = oldContext + context
+ // always check for cancellation of new context
+ newContext.checkCompletion()
+ // FAST PATH #1 -- new context is the same as the old one
+ if (newContext === oldContext) {
+ val coroutine = ScopeCoroutine(newContext, uCont)
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
+ // FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed)
+ // `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher)
+ if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
+ val coroutine = UndispatchedCoroutine(newContext, uCont)
+ // There are changes in the context, so this thread needs to be updated
+ withCoroutineContext(newContext, null) {
+ return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
+ }
+ }
+ // SLOW PATH -- use new dispatcher
+ val coroutine = DispatchedCoroutine(newContext, uCont)
+ coroutine.initParentJob()
+ block.startCoroutineCancellable(coroutine, coroutine)
+ coroutine.getResult()
}
- // SLOW PATH -- use new dispatcher
- val coroutine = DispatchedCoroutine(newContext, uCont)
- coroutine.initParentJob()
- block.startCoroutineCancellable(coroutine, coroutine)
- coroutine.getResult()
}
/**
@@ -167,7 +175,6 @@ public suspend fun withContext(
*
* This inline function calls [withContext].
*/
-@ExperimentalCoroutinesApi
public suspend inline operator fun CoroutineDispatcher.invoke(
noinline block: suspend CoroutineScope.() -> T
): T = withContext(this, block)
diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt
index fd5cd083e2..7d9315afbf 100644
--- a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt
+++ b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt
@@ -15,6 +15,8 @@ import kotlin.coroutines.intrinsics.*
* When the [cancel] function is explicitly invoked, this continuation immediately resumes with a [CancellationException] or
* the specified cancel cause.
*
+ * An instance of `CancellableContinuation` is created by the [suspendCancellableCoroutine] function.
+ *
* Cancellable continuation has three states (as subset of [Job] states):
*
* | **State** | [isActive] | [isCompleted] | [isCancelled] |
@@ -24,14 +26,12 @@ import kotlin.coroutines.intrinsics.*
* | _Canceled_ (final _completed_ state)| `false` | `true` | `true` |
*
* Invocation of [cancel] transitions this continuation from _active_ to _cancelled_ state, while
- * invocation of [resume] or [resumeWithException] transitions it from _active_ to _resumed_ state.
+ * invocation of [Continuation.resume] or [Continuation.resumeWithException] transitions it from _active_ to _resumed_ state.
*
* A [cancelled][isCancelled] continuation implies that it is [completed][isCompleted].
*
- * Invocation of [resume] or [resumeWithException] in _resumed_ state produces an [IllegalStateException].
- * Invocation of [resume] in _cancelled_ state is ignored (it is a trivial race between resume from the continuation owner and
- * outer job's cancellation, and the cancellation wins).
- * Invocation of [resumeWithException] in _cancelled_ state triggers exception handling of the passed exception.
+ * Invocation of [Continuation.resume] or [Continuation.resumeWithException] in _resumed_ state produces an [IllegalStateException],
+ * but is ignored in _cancelled_ state.
*
* ```
* +-----------+ resume +---------+
@@ -43,7 +43,6 @@ import kotlin.coroutines.intrinsics.*
* +-----------+
* | Cancelled |
* +-----------+
- *
* ```
*/
public interface CancellableContinuation : Continuation {
@@ -78,6 +77,14 @@ public interface CancellableContinuation : Continuation {
@InternalCoroutinesApi
public fun tryResume(value: T, idempotent: Any? = null): Any?
+ /**
+ * Same as [tryResume] but with [onCancellation] handler that called if and only if the value is not
+ * delivered to the caller because of the dispatch in the process, so that atomicity delivery
+ * guaranteed can be provided by having a cancellation fallback.
+ */
+ @InternalCoroutinesApi
+ public fun tryResume(value: T, idempotent: Any?, onCancellation: ((cause: Throwable) -> Unit)?): Any?
+
/**
* Tries to resume this continuation with the specified [exception] and returns a non-null object token if successful,
* or `null` otherwise (it was already resumed or cancelled). When a non-null object is returned,
@@ -100,14 +107,8 @@ public interface CancellableContinuation : Continuation {
* Legacy function that turned on cancellation behavior in [suspendCancellableCoroutine] before kotlinx.coroutines 1.1.0.
* This function does nothing and is left only for binary compatibility with old compiled code.
*
- * @suppress **Deprecated**: This function is no longer used.
- * It is left for binary compatibility with code compiled before kotlinx.coroutines 1.1.0.
+ * @suppress **This is unstable API and it is subject to change.**
*/
- @Deprecated(
- level = DeprecationLevel.HIDDEN,
- message = "This function is no longer used. " +
- "It is left for binary compatibility with code compiled before kotlinx.coroutines 1.1.0. "
- )
@InternalCoroutinesApi
public fun initCancellability()
@@ -118,8 +119,8 @@ public interface CancellableContinuation : Continuation {
public fun cancel(cause: Throwable? = null): Boolean
/**
- * Registers a [handler] to be **synchronously** invoked on cancellation (regular or exceptional) of this continuation.
- * When the continuation is already cancelled, the handler will be immediately invoked
+ * Registers a [handler] to be **synchronously** invoked on [cancellation][cancel] (regular or exceptional) of this continuation.
+ * When the continuation is already cancelled, the handler is immediately invoked
* with the cancellation exception. Otherwise, the handler will be invoked as soon as this
* continuation is cancelled.
*
@@ -128,7 +129,15 @@ public interface CancellableContinuation : Continuation {
* processed as an uncaught exception in the context of the current coroutine
* (see [CoroutineExceptionHandler]).
*
- * At most one [handler] can be installed on a continuation.
+ * At most one [handler] can be installed on a continuation. Attempt to call `invokeOnCancellation` second
+ * time produces [IllegalStateException].
+ *
+ * This handler is also called when this continuation [resumes][Continuation.resume] normally (with a value) and then
+ * is cancelled while waiting to be dispatched. More generally speaking, this handler is called whenever
+ * the caller of [suspendCancellableCoroutine] is getting a [CancellationException].
+ *
+ * A typical example for `invokeOnCancellation` usage is given in
+ * the documentation for the [suspendCancellableCoroutine] function.
*
* **Note**: Implementation of `CompletionHandler` must be fast, non-blocking, and thread-safe.
* This `handler` can be invoked concurrently with the surrounding code.
@@ -171,7 +180,7 @@ public interface CancellableContinuation : Continuation {
* (see [CoroutineExceptionHandler]).
*
* This function shall be used when resuming with a resource that must be closed by
- * code that called the corresponding suspending function, e.g.:
+ * code that called the corresponding suspending function, for example:
*
* ```
* continuation.resume(resource) {
@@ -179,70 +188,156 @@ public interface CancellableContinuation : Continuation {
* }
* ```
*
+ * A more complete example and further details are given in
+ * the documentation for the [suspendCancellableCoroutine] function.
+ *
* **Note**: The [onCancellation] handler must be fast, non-blocking, and thread-safe.
* It can be invoked concurrently with the surrounding code.
* There is no guarantee on the execution context of its invocation.
*/
- @ExperimentalCoroutinesApi // since 1.2.0, tentatively graduates in 1.3.0
- public fun resume(value: T, onCancellation: (cause: Throwable) -> Unit)
+ @ExperimentalCoroutinesApi // since 1.2.0
+ public fun resume(value: T, onCancellation: ((cause: Throwable) -> Unit)?)
}
/**
* Suspends the coroutine like [suspendCoroutine], but providing a [CancellableContinuation] to
- * the [block]. This function throws a [CancellationException] if the coroutine is cancelled or completed while suspended.
- */
-public suspend inline fun suspendCancellableCoroutine(
- crossinline block: (CancellableContinuation) -> Unit
-): T =
- suspendCoroutineUninterceptedOrReturn { uCont ->
- val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
- // NOTE: Before version 1.1.0 the following invocation was inlined here, so invocation of this
- // method indicates that the code was compiled by kotlinx.coroutines < 1.1.0
- // cancellable.initCancellability()
- block(cancellable)
- cancellable.getResult()
- }
-
-/**
- * Suspends the coroutine like [suspendCancellableCoroutine], but with *atomic cancellation*.
+ * the [block]. This function throws a [CancellationException] if the [Job] of the coroutine is
+ * cancelled or completed while it is suspended.
*
- * When the suspended function throws a [CancellationException], it means that the continuation was not resumed.
- * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may
- * continue to execute even after it was cancelled from the same thread in the case when the continuation
- * was already resumed and was posted for execution to the thread's queue.
+ * A typical use of this function is to suspend a coroutine while waiting for a result
+ * from a single-shot callback API and to return the result to the caller.
+ * For multi-shot callback APIs see [callbackFlow][kotlinx.coroutines.flow.callbackFlow].
*
- * @suppress **This an internal API and should not be used from general code.**
+ * ```
+ * suspend fun awaitCallback(): T = suspendCancellableCoroutine { continuation ->
+ * val callback = object : Callback { // Implementation of some callback interface
+ * override fun onCompleted(value: T) {
+ * // Resume coroutine with a value provided by the callback
+ * continuation.resume(value)
+ * }
+ * override fun onApiError(cause: Throwable) {
+ * // Resume coroutine with an exception provided by the callback
+ * continuation.resumeWithException(cause)
+ * }
+ * }
+ * // Register callback with an API
+ * api.register(callback)
+ * // Remove callback on cancellation
+ * continuation.invokeOnCancellation { api.unregister(callback) }
+ * // At this point the coroutine is suspended by suspendCancellableCoroutine until callback fires
+ * }
+ * ```
+ *
+ * > The callback `register`/`unregister` methods provided by an external API must be thread-safe, because
+ * > `invokeOnCancellation` block can be called at any time due to asynchronous nature of cancellation, even
+ * > concurrently with the call of the callback.
+ *
+ * ### Prompt cancellation guarantee
+ *
+ * This function provides **prompt cancellation guarantee**.
+ * If the [Job] of the current coroutine was cancelled while this function was suspended it will not resume
+ * successfully.
+ *
+ * The cancellation of the coroutine's job is generally asynchronous with respect to the suspended coroutine.
+ * The suspended coroutine is resumed with the call it to its [Continuation.resumeWith] member function or to
+ * [resume][Continuation.resume] extension function.
+ * However, when coroutine is resumed, it does not immediately start executing, but is passed to its
+ * [CoroutineDispatcher] to schedule its execution when dispatcher's resources become available for execution.
+ * The job's cancellation can happen both before, after, and concurrently with the call to `resume`. In any
+ * case, prompt cancellation guarantees that the the coroutine will not resume its code successfully.
+ *
+ * If the coroutine was resumed with an exception (for example, using [Continuation.resumeWithException] extension
+ * function) and cancelled, then the resulting exception of the `suspendCancellableCoroutine` function is determined
+ * by whichever action (exceptional resume or cancellation) that happened first.
+ *
+ * ### Returning resources from a suspended coroutine
+ *
+ * As a result of a prompt cancellation guarantee, when a closeable resource
+ * (like open file or a handle to another native resource) is returned from a suspended coroutine as a value
+ * it can be lost when the coroutine is cancelled. In order to ensure that the resource can be properly closed
+ * in this case, the [CancellableContinuation] interface provides two functions.
+ *
+ * * [invokeOnCancellation][CancellableContinuation.invokeOnCancellation] installs a handler that is called
+ * whenever a suspend coroutine is being cancelled. In addition to the example at the beginning, it can be
+ * used to ensure that a resource that was opened before the call to
+ * `suspendCancellableCoroutine` or in its body is closed in case of cancellation.
+ *
+ * ```
+ * suspendCancellableCoroutine { continuation ->
+ * val resource = openResource() // Opens some resource
+ * continuation.invokeOnCancellation {
+ * resource.close() // Ensures the resource is closed on cancellation
+ * }
+ * // ...
+ * }
+ * ```
+ *
+ * * [resume(value) { ... }][CancellableContinuation.resume] method on a [CancellableContinuation] takes
+ * an optional `onCancellation` block. It can be used when resuming with a resource that must be closed by
+ * the code that called the corresponding suspending function.
+ *
+ * ```
+ * suspendCancellableCoroutine { continuation ->
+ * val callback = object : Callback { // Implementation of some callback interface
+ * // A callback provides a reference to some closeable resource
+ * override fun onCompleted(resource: T) {
+ * // Resume coroutine with a value provided by the callback and ensure the resource is closed in case
+ * // when the coroutine is cancelled before the caller gets a reference to the resource.
+ * continuation.resume(resource) {
+ * resource.close() // Close the resource on cancellation
+ * }
+ * }
+ * // ...
+ * }
+ * ```
+ *
+ * ### Implementation details and custom continuation interceptors
+ *
+ * The prompt cancellation guarantee is the result of a coordinated implementation inside `suspendCancellableCoroutine`
+ * function and the [CoroutineDispatcher] class. The coroutine dispatcher checks for the status of the [Job] immediately
+ * before continuing its normal execution and aborts this normal execution, calling all the corresponding
+ * cancellation handlers, if the job was cancelled.
+ *
+ * If a custom implementation of [ContinuationInterceptor] is used in a coroutine's context that does not extend
+ * [CoroutineDispatcher] class, then there is no prompt cancellation guarantee. A custom continuation interceptor
+ * can resume execution of a previously suspended coroutine even if its job was already cancelled.
*/
-@InternalCoroutinesApi
-public suspend inline fun suspendAtomicCancellableCoroutine(
+public suspend inline fun suspendCancellableCoroutine(
crossinline block: (CancellableContinuation) -> Unit
): T =
suspendCoroutineUninterceptedOrReturn { uCont ->
- val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_ATOMIC_DEFAULT)
+ val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
+ /*
+ * For non-atomic cancellation we setup parent-child relationship immediately
+ * in case when `block` blocks the current thread (e.g. Rx2 with trampoline scheduler), but
+ * properly supports cancellation.
+ */
+ cancellable.initCancellability()
block(cancellable)
cancellable.getResult()
}
/**
- * Suspends coroutine similar to [suspendAtomicCancellableCoroutine], but an instance of [CancellableContinuationImpl] is reused if possible.
+ * Suspends the coroutine similar to [suspendCancellableCoroutine], but an instance of
+ * [CancellableContinuationImpl] is reused.
*/
-internal suspend inline fun suspendAtomicCancellableCoroutineReusable(
+internal suspend inline fun suspendCancellableCoroutineReusable(
crossinline block: (CancellableContinuation) -> Unit
): T = suspendCoroutineUninterceptedOrReturn { uCont ->
- val cancellable = getOrCreateCancellableContinuation(uCont.intercepted())
- block(cancellable)
- cancellable.getResult()
- }
+ val cancellable = getOrCreateCancellableContinuation(uCont.intercepted())
+ block(cancellable)
+ cancellable.getResult()
+}
internal fun getOrCreateCancellableContinuation(delegate: Continuation): CancellableContinuationImpl {
// If used outside of our dispatcher
if (delegate !is DispatchedContinuation) {
- return CancellableContinuationImpl(delegate, resumeMode = MODE_ATOMIC_DEFAULT)
+ return CancellableContinuationImpl(delegate, MODE_CANCELLABLE_REUSABLE)
}
/*
* Attempt to claim reusable instance.
*
- * suspendAtomicCancellableCoroutineReusable { // <- claimed
+ * suspendCancellableCoroutineReusable { // <- claimed
* // Any asynchronous cancellation is "postponed" while this block
* // is being executed
* } // postponed cancellation is checked here.
@@ -253,26 +348,13 @@ internal fun getOrCreateCancellableContinuation(delegate: Continuation):
* thus leaking CC instance for indefinite time.
* 2) Continuation was cancelled. Then we should prevent any further reuse and bail out.
*/
- return delegate.claimReusableCancellableContinuation()?.takeIf { it.resetState() }
- ?: return CancellableContinuationImpl(delegate, MODE_ATOMIC_DEFAULT)
+ return delegate.claimReusableCancellableContinuation()?.takeIf { it.resetStateReusable() }
+ ?: return CancellableContinuationImpl(delegate, MODE_CANCELLABLE_REUSABLE)
}
/**
- * @suppress **Deprecated**
- */
-@Deprecated(
- message = "holdCancellability parameter is deprecated and is no longer used",
- replaceWith = ReplaceWith("suspendAtomicCancellableCoroutine(block)")
-)
-@InternalCoroutinesApi
-public suspend inline fun suspendAtomicCancellableCoroutine(
- holdCancellability: Boolean = false,
- crossinline block: (CancellableContinuation) -> Unit
-): T =
- suspendAtomicCancellableCoroutine(block)
-
-/**
- * Removes the specified [node] on cancellation.
+ * Removes the specified [node] on cancellation. This function assumes that this node is already
+ * removed on successful resume and does not try to remove it if the continuation is cancelled during dispatch.
*/
internal fun CancellableContinuation<*>.removeOnCancellation(node: LockFreeLinkedListNode) =
invokeOnCancellation(handler = RemoveOnCancel(node).asHandler)
@@ -288,12 +370,12 @@ internal fun CancellableContinuation<*>.removeOnCancellation(node: LockFreeLinke
* @suppress **This an internal API and should not be used from general code.**
*/
@InternalCoroutinesApi
-public fun CancellableContinuation<*>.disposeOnCancellation(handle: DisposableHandle) =
+public fun CancellableContinuation<*>.disposeOnCancellation(handle: DisposableHandle): Unit =
invokeOnCancellation(handler = DisposeOnCancel(handle).asHandler)
// --------------- implementation details ---------------
-private class RemoveOnCancel(private val node: LockFreeLinkedListNode) : CancelHandler() {
+private class RemoveOnCancel(private val node: LockFreeLinkedListNode) : BeforeResumeCancelHandler() {
override fun invoke(cause: Throwable?) { node.remove() }
override fun toString() = "RemoveOnCancel[$node]"
}
diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt
index 1f67dd3c6c..cdb1b78882 100644
--- a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt
+++ b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt
@@ -27,13 +27,17 @@ internal open class CancellableContinuationImpl(
final override val delegate: Continuation,
resumeMode: Int
) : DispatchedTask(resumeMode), CancellableContinuation, CoroutineStackFrame {
+ init {
+ assert { resumeMode != MODE_UNINITIALIZED } // invalid mode for CancellableContinuationImpl
+ }
+
public override val context: CoroutineContext = delegate.context
/*
* Implementation notes
*
- * AbstractContinuation is a subset of Job with following limitations:
- * 1) It can have only cancellation listeners
+ * CancellableContinuationImpl is a subset of Job with following limitations:
+ * 1) It can have only cancellation listener (no "on cancelling")
* 2) It always invokes cancellation listener if it's cancelled (no 'invokeImmediately')
* 3) It can have at most one cancellation listener
* 4) Its cancellation listeners cannot be deregistered
@@ -82,21 +86,23 @@ internal open class CancellableContinuationImpl(
public override val isCancelled: Boolean get() = state is CancelledContinuation
public override fun initCancellability() {
- // This method does nothing. Leftover for binary compatibility with old compiled code
+ setupCancellation()
}
private fun isReusable(): Boolean = delegate is DispatchedContinuation<*> && delegate.isReusable(this)
/**
- * Resets cancellability state in order to [suspendAtomicCancellableCoroutineReusable] to work.
- * Invariant: used only by [suspendAtomicCancellableCoroutineReusable] in [REUSABLE_CLAIMED] state.
+ * Resets cancellability state in order to [suspendCancellableCoroutineReusable] to work.
+ * Invariant: used only by [suspendCancellableCoroutineReusable] in [REUSABLE_CLAIMED] state.
*/
- @JvmName("resetState") // Prettier stack traces
- internal fun resetState(): Boolean {
+ @JvmName("resetStateReusable") // Prettier stack traces
+ internal fun resetStateReusable(): Boolean {
+ assert { resumeMode == MODE_CANCELLABLE_REUSABLE } // invalid mode for CancellableContinuationImpl
assert { parentHandle !== NonDisposableHandle }
val state = _state.value
assert { state !is NotCompleted }
- if (state is CompletedIdempotentResult) {
+ if (state is CompletedContinuation && state.idempotentResume != null) {
+ // Cannot reuse continuation that was resumed with idempotent marker
detachChild()
return false
}
@@ -107,13 +113,13 @@ internal open class CancellableContinuationImpl(
/**
* Setups parent cancellation and checks for postponed cancellation in the case of reusable continuations.
- * It is only invoked from an internal [getResult] function.
+ * It is only invoked from an internal [getResult] function for reusable continuations
+ * and from [suspendCancellableCoroutine] to establish a cancellation before registering CC anywhere.
*/
private fun setupCancellation() {
if (checkCompleted()) return
if (parentHandle !== null) return // fast path 2 -- was already initialized
val parent = delegate.context[Job] ?: return // fast path 3 -- don't do anything without parent
- parent.start() // make sure the parent is started
val handle = parent.invokeOnCompletion(
onCancelling = true,
handler = ChildContinuation(parent, this).asHandler
@@ -129,7 +135,7 @@ internal open class CancellableContinuationImpl(
private fun checkCompleted(): Boolean {
val completed = isCompleted
- if (resumeMode != MODE_ATOMIC_DEFAULT) return completed // Do not check postponed cancellation for non-reusable continuations
+ if (!resumeMode.isReusableMode) return completed // Do not check postponed cancellation for non-reusable continuations
val dispatched = delegate as? DispatchedContinuation<*> ?: return completed
val cause = dispatched.checkPostponedCancellation(this) ?: return completed
if (!completed) {
@@ -146,10 +152,26 @@ internal open class CancellableContinuationImpl(
override fun takeState(): Any? = state
- override fun cancelResult(state: Any?, cause: Throwable) {
- if (state is CompletedWithCancellation) {
- invokeHandlerSafely {
- state.onCancellation(cause)
+ // Note: takeState does not clear the state so we don't use takenState
+ // and we use the actual current state where in CAS-loop
+ override fun cancelCompletedResult(takenState: Any?, cause: Throwable): Unit = _state.loop { state ->
+ when (state) {
+ is NotCompleted -> error("Not completed")
+ is CompletedExceptionally -> return // already completed exception or cancelled, nothing to do
+ is CompletedContinuation -> {
+ check(!state.cancelled) { "Must be called at most once" }
+ val update = state.copy(cancelCause = cause)
+ if (_state.compareAndSet(state, update)) {
+ state.invokeHandlers(this, cause)
+ return // done
+ }
+ }
+ else -> {
+ // completed normally without marker class, promote to CompletedContinuation in case
+ // if invokeOnCancellation if called later
+ if (_state.compareAndSet(state, CompletedContinuation(state, cancelCause = cause))) {
+ return // done
+ }
}
}
}
@@ -158,7 +180,7 @@ internal open class CancellableContinuationImpl(
* Attempt to postpone cancellation for reusable cancellable continuation
*/
private fun cancelLater(cause: Throwable): Boolean {
- if (resumeMode != MODE_ATOMIC_DEFAULT) return false
+ if (!resumeMode.isReusableMode) return false
val dispatched = (delegate as? DispatchedContinuation<*>) ?: return false
return dispatched.postponeCancellation(cause)
}
@@ -170,10 +192,10 @@ internal open class CancellableContinuationImpl(
val update = CancelledContinuation(this, cause, handled = state is CancelHandler)
if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure
// Invoke cancel handler if it was present
- if (state is CancelHandler) invokeHandlerSafely { state.invoke(cause) }
+ (state as? CancelHandler)?.let { callCancelHandler(it, cause) }
// Complete state update
detachChildIfNonResuable()
- dispatchResume(mode = MODE_ATOMIC_DEFAULT)
+ dispatchResume(resumeMode) // no need for additional cancellation checks
return true
}
}
@@ -185,14 +207,36 @@ internal open class CancellableContinuationImpl(
detachChildIfNonResuable()
}
- private inline fun invokeHandlerSafely(block: () -> Unit) {
+ private inline fun callCancelHandlerSafely(block: () -> Unit) {
try {
- block()
+ block()
} catch (ex: Throwable) {
// Handler should never fail, if it does -- it is an unhandled exception
handleCoroutineException(
context,
- CompletionHandlerException("Exception in cancellation handler for $this", ex)
+ CompletionHandlerException("Exception in invokeOnCancellation handler for $this", ex)
+ )
+ }
+ }
+
+ private fun callCancelHandler(handler: CompletionHandler, cause: Throwable?) =
+ /*
+ * :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension,
+ * because we play type tricks on Kotlin/JS and handler is not necessarily a function there
+ */
+ callCancelHandlerSafely { handler.invokeIt(cause) }
+
+ fun callCancelHandler(handler: CancelHandler, cause: Throwable?) =
+ callCancelHandlerSafely { handler.invoke(cause) }
+
+ fun callOnCancellation(onCancellation: (cause: Throwable) -> Unit, cause: Throwable) {
+ try {
+ onCancellation.invoke(cause)
+ } catch (ex: Throwable) {
+ // Handler should never fail, if it does -- it is an unhandled exception
+ handleCoroutineException(
+ context,
+ CompletionHandlerException("Exception in resume onCancellation handler for $this", ex)
)
}
}
@@ -231,64 +275,75 @@ internal open class CancellableContinuationImpl(
val state = this.state
if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this)
// if the parent job was already cancelled, then throw the corresponding cancellation exception
- // otherwise, there is a race is suspendCancellableCoroutine { cont -> ... } does cont.resume(...)
+ // otherwise, there is a race if suspendCancellableCoroutine { cont -> ... } does cont.resume(...)
// before the block returns. This getResult would return a result as opposed to cancellation
// exception that should have happened if the continuation is dispatched for execution later.
- if (resumeMode == MODE_CANCELLABLE) {
+ if (resumeMode.isCancellableMode) {
val job = context[Job]
if (job != null && !job.isActive) {
val cause = job.getCancellationException()
- cancelResult(state, cause)
+ cancelCompletedResult(state, cause)
throw recoverStackTrace(cause, this)
}
}
return getSuccessfulResult(state)
}
- override fun resumeWith(result: Result) {
+ override fun resumeWith(result: Result) =
resumeImpl(result.toState(this), resumeMode)
- }
- override fun resume(value: T, onCancellation: (cause: Throwable) -> Unit) {
- val cancelled = resumeImpl(CompletedWithCancellation(value, onCancellation), resumeMode)
- if (cancelled != null) {
- // too late to resume (was cancelled) -- call handler
- invokeHandlerSafely {
- onCancellation(cancelled.cause)
- }
- }
- }
+ override fun resume(value: T, onCancellation: ((cause: Throwable) -> Unit)?) =
+ resumeImpl(value, resumeMode, onCancellation)
public override fun invokeOnCancellation(handler: CompletionHandler) {
- var handleCache: CancelHandler? = null
+ val cancelHandler = makeCancelHandler(handler)
_state.loop { state ->
when (state) {
is Active -> {
- val node = handleCache ?: makeHandler(handler).also { handleCache = it }
- if (_state.compareAndSet(state, node)) return // quit on cas success
+ if (_state.compareAndSet(state, cancelHandler)) return // quit on cas success
}
is CancelHandler -> multipleHandlersError(handler, state)
- is CancelledContinuation -> {
+ is CompletedExceptionally -> {
/*
- * Continuation was already cancelled, invoke directly.
+ * Continuation was already cancelled or completed exceptionally.
* NOTE: multiple invokeOnCancellation calls with different handlers are not allowed,
- * so we check to make sure that handler was installed just once.
+ * so we check to make sure handler was installed just once.
*/
if (!state.makeHandled()) multipleHandlersError(handler, state)
/*
+ * Call the handler only if it was cancelled (not called when completed exceptionally).
* :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension,
* because we play type tricks on Kotlin/JS and handler is not necessarily a function there
*/
- invokeHandlerSafely { handler.invokeIt((state as? CompletedExceptionally)?.cause) }
+ if (state is CancelledContinuation) {
+ callCancelHandler(handler, (state as? CompletedExceptionally)?.cause)
+ }
return
}
+ is CompletedContinuation -> {
+ /*
+ * Continuation was already completed, and might already have cancel handler.
+ */
+ if (state.cancelHandler != null) multipleHandlersError(handler, state)
+ // BeforeResumeCancelHandler does not need to be called on a completed continuation
+ if (cancelHandler is BeforeResumeCancelHandler) return
+ if (state.cancelled) {
+ // Was already cancelled while being dispatched -- invoke the handler directly
+ callCancelHandler(handler, state.cancelCause)
+ return
+ }
+ val update = state.copy(cancelHandler = cancelHandler)
+ if (_state.compareAndSet(state, update)) return // quit on cas success
+ }
else -> {
/*
- * Continuation was already completed, do nothing.
- * NOTE: multiple invokeOnCancellation calls with different handlers are not allowed,
- * but we have no way to check that it was installed just once in this case.
+ * Continuation was already completed normally, but might get cancelled while being dispatched.
+ * Change its state to CompletedContinuation, unless we have BeforeResumeCancelHandler which
+ * does not need to be called in this case.
*/
- return
+ if (cancelHandler is BeforeResumeCancelHandler) return
+ val update = CompletedContinuation(state, cancelHandler = cancelHandler)
+ if (_state.compareAndSet(state, update)) return // quit on cas success
}
}
}
@@ -298,7 +353,7 @@ internal open class CancellableContinuationImpl(
error("It's prohibited to register multiple handlers, tried to register $handler, already has $state")
}
- private fun makeHandler(handler: CompletionHandler): CancelHandler =
+ private fun makeCancelHandler(handler: CompletionHandler): CancelHandler =
if (handler is CancelHandler) handler else InvokeOnCancel(handler)
private fun dispatchResume(mode: Int) {
@@ -307,15 +362,39 @@ internal open class CancellableContinuationImpl(
dispatch(mode)
}
- // returns null when successfully dispatched resumed, CancelledContinuation if too late (was already cancelled)
- private fun resumeImpl(proposedUpdate: Any?, resumeMode: Int): CancelledContinuation? {
+ private fun resumedState(
+ state: NotCompleted,
+ proposedUpdate: Any?,
+ resumeMode: Int,
+ onCancellation: ((cause: Throwable) -> Unit)?,
+ idempotent: Any?
+ ): Any? = when {
+ proposedUpdate is CompletedExceptionally -> {
+ assert { idempotent == null } // there are no idempotent exceptional resumes
+ assert { onCancellation == null } // only successful results can be cancelled
+ proposedUpdate
+ }
+ !resumeMode.isCancellableMode && idempotent == null -> proposedUpdate // cannot be cancelled in process, all is fine
+ onCancellation != null || (state is CancelHandler && state !is BeforeResumeCancelHandler) || idempotent != null ->
+ // mark as CompletedContinuation if special cases are present:
+ // Cancellation handlers that shall be called after resume or idempotent resume
+ CompletedContinuation(proposedUpdate, state as? CancelHandler, onCancellation, idempotent)
+ else -> proposedUpdate // simple case -- use the value directly
+ }
+
+ private fun resumeImpl(
+ proposedUpdate: Any?,
+ resumeMode: Int,
+ onCancellation: ((cause: Throwable) -> Unit)? = null
+ ) {
_state.loop { state ->
when (state) {
is NotCompleted -> {
- if (!_state.compareAndSet(state, proposedUpdate)) return@loop // retry on cas failure
+ val update = resumedState(state, proposedUpdate, resumeMode, onCancellation, idempotent = null)
+ if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure
detachChildIfNonResuable()
- dispatchResume(resumeMode)
- return null
+ dispatchResume(resumeMode) // dispatch resume, but it might get cancelled in process
+ return // done
}
is CancelledContinuation -> {
/*
@@ -323,14 +402,48 @@ internal open class CancellableContinuationImpl(
* because cancellation is asynchronous and may race with resume.
* Racy exceptions will be lost, too.
*/
- if (state.makeResumed()) return state // tried to resume just once, but was cancelled
+ if (state.makeResumed()) { // check if trying to resume one (otherwise error)
+ // call onCancellation
+ onCancellation?.let { callOnCancellation(it, state.cause) }
+ return // done
+ }
+ }
+ }
+ alreadyResumedError(proposedUpdate) // otherwise, an error (second resume attempt)
+ }
+ }
+
+ /**
+ * Similar to [tryResume], but does not actually completes resume (needs [completeResume] call).
+ * Returns [RESUME_TOKEN] when resumed, `null` when it was already resumed or cancelled.
+ */
+ private fun tryResumeImpl(
+ proposedUpdate: Any?,
+ idempotent: Any?,
+ onCancellation: ((cause: Throwable) -> Unit)?
+ ): Symbol? {
+ _state.loop { state ->
+ when (state) {
+ is NotCompleted -> {
+ val update = resumedState(state, proposedUpdate, resumeMode, onCancellation, idempotent)
+ if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure
+ detachChildIfNonResuable()
+ return RESUME_TOKEN
+ }
+ is CompletedContinuation -> {
+ return if (idempotent != null && state.idempotentResume === idempotent) {
+ assert { state.result == proposedUpdate } // "Non-idempotent resume"
+ RESUME_TOKEN // resumed with the same token -- ok
+ } else {
+ null // resumed with a different token or non-idempotent -- too late
+ }
}
+ else -> return null // cannot resume -- not active anymore
}
- alreadyResumedError(proposedUpdate) // otherwise -- an error (second resume attempt)
}
}
- private fun alreadyResumedError(proposedUpdate: Any?) {
+ private fun alreadyResumedError(proposedUpdate: Any?): Nothing {
error("Already resumed, but proposed with update $proposedUpdate")
}
@@ -342,7 +455,7 @@ internal open class CancellableContinuationImpl(
/**
* Detaches from the parent.
- * Invariant: used used from [CoroutineDispatcher.releaseInterceptedContinuation] iff [isReusable] is `true`
+ * Invariant: used from [CoroutineDispatcher.releaseInterceptedContinuation] iff [isReusable] is `true`
*/
internal fun detachChild() {
val handle = parentHandle
@@ -351,42 +464,14 @@ internal open class CancellableContinuationImpl(
}
// Note: Always returns RESUME_TOKEN | null
- override fun tryResume(value: T, idempotent: Any?): Any? {
- _state.loop { state ->
- when (state) {
- is NotCompleted -> {
- val update: Any? = if (idempotent == null) value else
- CompletedIdempotentResult(idempotent, value)
- if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure
- detachChildIfNonResuable()
- return RESUME_TOKEN
- }
- is CompletedIdempotentResult -> {
- return if (state.idempotentResume === idempotent) {
- assert { state.result === value } // "Non-idempotent resume"
- RESUME_TOKEN
- } else {
- null
- }
- }
- else -> return null // cannot resume -- not active anymore
- }
- }
- }
+ override fun tryResume(value: T, idempotent: Any?): Any? =
+ tryResumeImpl(value, idempotent, onCancellation = null)
- override fun tryResumeWithException(exception: Throwable): Any? {
- _state.loop { state ->
- when (state) {
- is NotCompleted -> {
- val update = CompletedExceptionally(exception)
- if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure
- detachChildIfNonResuable()
- return RESUME_TOKEN
- }
- else -> return null // cannot resume -- not active anymore
- }
- }
- }
+ override fun tryResume(value: T, idempotent: Any?, onCancellation: ((cause: Throwable) -> Unit)?): Any? =
+ tryResumeImpl(value, idempotent, onCancellation)
+
+ override fun tryResumeWithException(exception: Throwable): Any? =
+ tryResumeImpl(CompletedExceptionally(exception), idempotent = null, onCancellation = null)
// note: token is always RESUME_TOKEN
override fun completeResume(token: Any) {
@@ -407,11 +492,15 @@ internal open class CancellableContinuationImpl(
@Suppress("UNCHECKED_CAST")
override fun getSuccessfulResult(state: Any?): T =
when (state) {
- is CompletedIdempotentResult -> state.result as T
- is CompletedWithCancellation -> state.result as T
+ is CompletedContinuation -> state.result as T
else -> state as T
}
+ // The exceptional state in CancellableContinuationImpl is stored directly and it is not recovered yet.
+ // The stacktrace recovery is invoked here.
+ override fun getExceptionalResult(state: Any?): Throwable? =
+ super.getExceptionalResult(state)?.let { recoverStackTrace(it, delegate) }
+
// For nicer debugging
public override fun toString(): String =
"${nameString()}(${delegate.toDebugString()}){$state}@$hexAddress"
@@ -428,8 +517,20 @@ private object Active : NotCompleted {
override fun toString(): String = "Active"
}
+/**
+ * Base class for all [CancellableContinuation.invokeOnCancellation] handlers to avoid an extra instance
+ * on JVM, yet support JS where you cannot extend from a functional type.
+ */
internal abstract class CancelHandler : CancelHandlerBase(), NotCompleted
+/**
+ * Base class for all [CancellableContinuation.invokeOnCancellation] handlers that don't need to be invoked
+ * if continuation is cancelled after resumption, during dispatch, because the corresponding resources
+ * were already released before calling `resume`. This cancel handler is called only before `resume`.
+ * It avoids allocation of [CompletedContinuation] instance during resume on JVM.
+ */
+internal abstract class BeforeResumeCancelHandler : CancelHandler()
+
// Wrapper for lambdas, for the performance sake CancelHandler can be subclassed directly
private class InvokeOnCancel( // Clashes with InvokeOnCancellation
private val handler: CompletionHandler
@@ -440,17 +541,18 @@ private class InvokeOnCancel( // Clashes with InvokeOnCancellation
override fun toString() = "InvokeOnCancel[${handler.classSimpleName}@$hexAddress]"
}
-private class CompletedIdempotentResult(
- @JvmField val idempotentResume: Any?,
- @JvmField val result: Any?
-) {
- override fun toString(): String = "CompletedIdempotentResult[$result]"
-}
-
-private class CompletedWithCancellation(
+// Completed with additional metadata
+private data class CompletedContinuation(
@JvmField val result: Any?,
- @JvmField val onCancellation: (cause: Throwable) -> Unit
+ @JvmField val cancelHandler: CancelHandler? = null, // installed via invokeOnCancellation
+ @JvmField val onCancellation: ((cause: Throwable) -> Unit)? = null, // installed via resume block
+ @JvmField val idempotentResume: Any? = null,
+ @JvmField val cancelCause: Throwable? = null
) {
- override fun toString(): String = "CompletedWithCancellation[$result]"
-}
+ val cancelled: Boolean get() = cancelCause != null
+ fun invokeHandlers(cont: CancellableContinuationImpl<*>, cause: Throwable) {
+ cancelHandler?.let { cont.callCancelHandler(it, cause) }
+ onCancellation?.let { cont.callOnCancellation(it, cause) }
+ }
+}
diff --git a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt
index f6cf90d515..2f00847298 100644
--- a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt
+++ b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt
@@ -16,8 +16,11 @@ import kotlinx.coroutines.selects.*
*
* An instance of completable deferred can be created by `CompletableDeferred()` function in _active_ state.
*
- * All functions on this interface and on all interfaces derived from it are **thread-safe** and can
+ * All functions on this interface are **thread-safe** and can
* be safely invoked from concurrent coroutines without external synchronization.
+ *
+ * **The `CompletableDeferred` interface is not stable for inheritance in 3rd party libraries**,
+ * as new methods might be added to this interface in the future, but is stable for use.
*/
public interface CompletableDeferred : Deferred {
/**
@@ -54,8 +57,8 @@ public interface CompletableDeferred : Deferred {
* This function transitions this deferred in the same ways described by [CompletableDeferred.complete] and
* [CompletableDeferred.completeExceptionally].
*/
-@ExperimentalCoroutinesApi // since 1.3.2, tentatively until 1.4.0
-public fun CompletableDeferred.completeWith(result: Result) = result.fold({ complete(it) }, { completeExceptionally(it) })
+public fun CompletableDeferred.completeWith(result: Result): Boolean =
+ result.fold({ complete(it) }, { completeExceptionally(it) })
/**
* Creates a [CompletableDeferred] in an _active_ state.
diff --git a/kotlinx-coroutines-core/common/src/CompletableJob.kt b/kotlinx-coroutines-core/common/src/CompletableJob.kt
index 4b4d16bc53..74a92e36e5 100644
--- a/kotlinx-coroutines-core/common/src/CompletableJob.kt
+++ b/kotlinx-coroutines-core/common/src/CompletableJob.kt
@@ -7,6 +7,12 @@ package kotlinx.coroutines
/**
* A job that can be completed using [complete()] function.
* It is returned by [Job()][Job] and [SupervisorJob()][SupervisorJob] constructor functions.
+ *
+ * All functions on this interface are **thread-safe** and can
+ * be safely invoked from concurrent coroutines without external synchronization.
+ *
+ * **The `CompletableJob` interface is not stable for inheritance in 3rd party libraries**,
+ * as new methods might be added to this interface in the future, but is stable for use.
*/
public interface CompletableJob : Job {
/**
diff --git a/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt b/kotlinx-coroutines-core/common/src/CompletionState.kt
similarity index 78%
rename from kotlinx-coroutines-core/common/src/CompletedExceptionally.kt
rename to kotlinx-coroutines-core/common/src/CompletionState.kt
index b426785bd7..f09aa3ccd9 100644
--- a/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt
+++ b/kotlinx-coroutines-core/common/src/CompletionState.kt
@@ -9,10 +9,17 @@ import kotlinx.coroutines.internal.*
import kotlin.coroutines.*
import kotlin.jvm.*
-internal fun Result.toState(): Any? = fold({ it }, { CompletedExceptionally(it) })
+internal fun Result.toState(
+ onCancellation: ((cause: Throwable) -> Unit)? = null
+): Any? = fold(
+ onSuccess = { if (onCancellation != null) CompletedWithCancellation(it, onCancellation) else it },
+ onFailure = { CompletedExceptionally(it) }
+)
-internal fun Result.toState(caller: CancellableContinuation<*>): Any? = fold({ it },
- { CompletedExceptionally(recoverStackTrace(it, caller)) })
+internal fun Result.toState(caller: CancellableContinuation<*>): Any? = fold(
+ onSuccess = { it },
+ onFailure = { CompletedExceptionally(recoverStackTrace(it, caller)) }
+)
@Suppress("RESULT_CLASS_IN_RETURN_TYPE", "UNCHECKED_CAST")
internal fun recoverResult(state: Any?, uCont: Continuation): Result =
@@ -21,6 +28,11 @@ internal fun recoverResult(state: Any?, uCont: Continuation): Result =
else
Result.success(state as T)
+internal data class CompletedWithCancellation(
+ @JvmField val result: Any?,
+ @JvmField val onCancellation: (cause: Throwable) -> Unit
+)
+
/**
* Class for an internal state of a job that was cancelled (completed exceptionally).
*
diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt
index fe4c263e18..ab1e814b8a 100644
--- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt
+++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt
@@ -4,6 +4,7 @@
package kotlinx.coroutines
+import kotlinx.coroutines.internal.*
import kotlin.coroutines.*
/**
@@ -87,7 +88,7 @@ public abstract class CoroutineDispatcher :
* @suppress **This an internal API and should not be used from general code.**
*/
@InternalCoroutinesApi
- public open fun dispatchYield(context: CoroutineContext, block: Runnable) = dispatch(context, block)
+ public open fun dispatchYield(context: CoroutineContext, block: Runnable): Unit = dispatch(context, block)
/**
* Returns a continuation that wraps the provided [continuation], thus intercepting all resumptions.
@@ -115,7 +116,7 @@ public abstract class CoroutineDispatcher :
"The dispatcher to the right of `+` just replaces the dispatcher to the left.",
level = DeprecationLevel.ERROR
)
- public operator fun plus(other: CoroutineDispatcher) = other
+ public operator fun plus(other: CoroutineDispatcher): CoroutineDispatcher = other
/** @suppress for nicer debugging */
override fun toString(): String = "$classSimpleName@$hexAddress"
diff --git a/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt b/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt
index cd7fd0d7ca..b49a6faa35 100644
--- a/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt
+++ b/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt
@@ -52,11 +52,38 @@ public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineConte
}
/**
- * An optional element in the coroutine context to handle uncaught exceptions.
+ * An optional element in the coroutine context to handle **uncaught** exceptions.
*
- * Normally, uncaught exceptions can only result from coroutines created using the [launch][CoroutineScope.launch] builder.
+ * Normally, uncaught exceptions can only result from _root_ coroutines created using the [launch][CoroutineScope.launch] builder.
+ * All _children_ coroutines (coroutines created in the context of another [Job]) delegate handling of their
+ * exceptions to their parent coroutine, which also delegates to the parent, and so on until the root,
+ * so the `CoroutineExceptionHandler` installed in their context is never used.
+ * Coroutines running with [SupervisorJob] do not propagate exceptions to their parent and are treated like root coroutines.
* A coroutine that was created using [async][CoroutineScope.async] always catches all its exceptions and represents them
- * in the resulting [Deferred] object.
+ * in the resulting [Deferred] object, so it cannot result in uncaught exceptions.
+ *
+ * ### Handling coroutine exceptions
+ *
+ * `CoroutineExceptionHandler` is a last-resort mechanism for global "catch all" behavior.
+ * You cannot recover from the exception in the `CoroutineExceptionHandler`. The coroutine had already completed
+ * with the corresponding exception when the handler is called. Normally, the handler is used to
+ * log the exception, show some kind of error message, terminate, and/or restart the application.
+ *
+ * If you need to handle exception in a specific part of the code, it is recommended to use `try`/`catch` around
+ * the corresponding code inside your coroutine. This way you can prevent completion of the coroutine
+ * with the exception (exception is now _caught_), retry the operation, and/or take other arbitrary actions:
+ *
+ * ```
+ * scope.launch { // launch child coroutine in a scope
+ * try {
+ * // do something
+ * } catch (e: Throwable) {
+ * // handle exception
+ * }
+ * }
+ * ```
+ *
+ * ### Implementation details
*
* By default, when no handler is installed, uncaught exception are handled in the following way:
* * If exception is [CancellationException] then it is ignored
@@ -66,10 +93,7 @@ public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineConte
* * Otherwise, all instances of [CoroutineExceptionHandler] found via [ServiceLoader]
* * and current thread's [Thread.uncaughtExceptionHandler] are invoked.
*
- * [CoroutineExceptionHandler] can be invoked from an arbitrary dispatcher used by coroutines in the current job hierarchy.
- * For example, if one has a `MainScope` and launches children of the scope in main and default dispatchers, then exception handler can
- * be invoked either in main or in default dispatcher thread regardless of
- * which particular dispatcher coroutine that has thrown an exception used.
+ * [CoroutineExceptionHandler] can be invoked from an arbitrary thread.
*/
public interface CoroutineExceptionHandler : CoroutineContext.Element {
/**
diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt
index a6b79bdb5a..0dde6c9352 100644
--- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt
+++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt
@@ -1,46 +1,60 @@
/*
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
+@file:OptIn(ExperimentalContracts::class)
package kotlinx.coroutines
import kotlinx.coroutines.internal.*
import kotlinx.coroutines.intrinsics.*
+import kotlin.contracts.*
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*
/**
- * Defines a scope for new coroutines. Every coroutine builder
+ * Defines a scope for new coroutines. Every **coroutine builder** (like [launch], [async], etc)
* is an extension on [CoroutineScope] and inherits its [coroutineContext][CoroutineScope.coroutineContext]
- * to automatically propagate both context elements and cancellation.
+ * to automatically propagate all its elements and cancellation.
*
* The best ways to obtain a standalone instance of the scope are [CoroutineScope()] and [MainScope()] factory functions.
* Additional context elements can be appended to the scope using the [plus][CoroutineScope.plus] operator.
*
+ * ### Convention for structured concurrency
+ *
* Manual implementation of this interface is not recommended, implementation by delegation should be preferred instead.
- * By convention, the [context of a scope][CoroutineScope.coroutineContext] should contain an instance of a [job][Job] to enforce structured concurrency.
+ * By convention, the [context of a scope][CoroutineScope.coroutineContext] should contain an instance of a
+ * [job][Job] to enforce the discipline of **structured concurrency** with propagation of cancellation.
*
- * Every coroutine builder (like [launch][CoroutineScope.launch], [async][CoroutineScope.async], etc)
+ * Every coroutine builder (like [launch], [async], etc)
* and every scoping function (like [coroutineScope], [withContext], etc) provides _its own_ scope
* with its own [Job] instance into the inner block of code it runs.
* By convention, they all wait for all the coroutines inside their block to complete before completing themselves,
- * thus enforcing the discipline of **structured concurrency**.
+ * thus enforcing the structured concurrency. See [Job] documentation for more details.
*
- * [CoroutineScope] should be implemented (or used as a field) on entities with a well-defined lifecycle that are responsible
- * for launching children coroutines. Example of such entity on Android is Activity.
- * Usage of this interface may look like this:
+ * ### Android usage
+ *
+ * Android has first-party support for coroutine scope in all entities with the lifecycle.
+ * See [the corresponding documentation](https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescope).
+ *
+ * ### Custom usage
+ *
+ * [CoroutineScope] should be implemented or declared as a property on entities with a well-defined lifecycle that are
+ * responsible for launching children coroutines, for example:
*
* ```
- * class MyActivity : AppCompatActivity(), CoroutineScope by MainScope() {
- * override fun onDestroy() {
- * cancel() // cancel is extension on CoroutineScope
+ * class MyUIClass {
+ * val scope = MainScope() // the scope of MyUIClass
+ *
+ * fun destroy() { // destroys an instance of MyUIClass
+ * scope.cancel() // cancels all coroutines launched in this scope
+ * // ... do the rest of cleanup here ...
* }
*
* /*
- * * Note how coroutine builders are scoped: if activity is destroyed or any of the launched coroutines
+ * * Note: if this instance is destroyed or any of the launched coroutines
* * in this method throws an exception, then all nested coroutines are cancelled.
* */
- * fun showSomeData() = launch { // <- extension on current activity, launched in the main thread
+ * fun showSomeData() = scope.launch { // launched in the main thread
* // ... here we can use suspending functions or coroutine builders with other dispatchers
* draw(data) // draw in the main thread
* }
@@ -171,11 +185,15 @@ public object GlobalScope : CoroutineScope {
* or may throw a corresponding unhandled [Throwable] if there is any unhandled exception in this scope
* (for example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope).
*/
-public suspend fun coroutineScope(block: suspend CoroutineScope.() -> R): R =
- suspendCoroutineUninterceptedOrReturn { uCont ->
+public suspend fun coroutineScope(block: suspend CoroutineScope.() -> R): R {
+ contract {
+ callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+ }
+ return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
+}
/**
* Creates a [CoroutineScope] that wraps the given coroutine [context].
@@ -208,10 +226,10 @@ public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Uni
/**
* Ensures that current scope is [active][CoroutineScope.isActive].
- * Throws [IllegalStateException] if the context does not have a job in it.
*
* If the job is no longer active, throws [CancellationException].
* If the job was cancelled, thrown exception contains the original cancellation cause.
+ * This function does not do anything if there is no [Job] in the scope's [coroutineContext][CoroutineScope.coroutineContext].
*
* This method is a drop-in replacement for the following code, but with more precise exception:
* ```
@@ -219,5 +237,23 @@ public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Uni
* throw CancellationException()
* }
* ```
+ *
+ * @see CoroutineContext.ensureActive
*/
public fun CoroutineScope.ensureActive(): Unit = coroutineContext.ensureActive()
+
+
+/**
+ * Returns the current [CoroutineContext] retrieved by using [kotlin.coroutines.coroutineContext].
+ * This function is an alias to avoid name clash with [CoroutineScope.coroutineContext] in a receiver position:
+ *
+ * ```
+ * launch { // this: CoroutineScope
+ * val flow = flow {
+ * coroutineContext // Resolves into the context of outer launch, which is incorrect, see KT-38033
+ * currentCoroutineContext() // Retrieves actual context where the flow is collected
+ * }
+ * }
+ * ```
+ */
+public suspend inline fun currentCoroutineContext(): CoroutineContext = coroutineContext
diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt
index 1272ce7c3a..d5791c79fe 100644
--- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt
+++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt
@@ -1,7 +1,7 @@
/*
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-
+@file:Suppress("NO_EXPLICIT_VISIBILITY_IN_API_MODE")
package kotlinx.coroutines
import kotlinx.coroutines.CoroutineStart.*
@@ -55,7 +55,7 @@ public enum class CoroutineStart {
* Cancellability of coroutine at suspension points depends on the particular implementation details of
* suspending functions as in [DEFAULT].
*/
- @ExperimentalCoroutinesApi
+ @ExperimentalCoroutinesApi // Since 1.0.0, no ETA on stability
ATOMIC,
/**
@@ -71,7 +71,7 @@ public enum class CoroutineStart {
*
* **Note: This is an experimental api.** Execution semantics of coroutines may change in the future when this mode is used.
*/
- @ExperimentalCoroutinesApi
+ @ExperimentalCoroutinesApi // Since 1.0.0, no ETA on stability
UNDISPATCHED;
/**
@@ -85,12 +85,12 @@ public enum class CoroutineStart {
* @suppress **This an internal API and should not be used from general code.**
*/
@InternalCoroutinesApi
- public operator fun invoke(block: suspend () -> T, completion: Continuation) =
+ public operator fun invoke(block: suspend () -> T, completion: Continuation): Unit =
when (this) {
- CoroutineStart.DEFAULT -> block.startCoroutineCancellable(completion)
- CoroutineStart.ATOMIC -> block.startCoroutine(completion)
- CoroutineStart.UNDISPATCHED -> block.startCoroutineUndispatched(completion)
- CoroutineStart.LAZY -> Unit // will start lazily
+ DEFAULT -> block.startCoroutineCancellable(completion)
+ ATOMIC -> block.startCoroutine(completion)
+ UNDISPATCHED -> block.startCoroutineUndispatched(completion)
+ LAZY -> Unit // will start lazily
}
/**
@@ -104,12 +104,12 @@ public enum class CoroutineStart {
* @suppress **This an internal API and should not be used from general code.**
*/
@InternalCoroutinesApi
- public operator fun invoke(block: suspend R.() -> T, receiver: R, completion: Continuation) =
+ public operator fun invoke(block: suspend R.() -> T, receiver: R, completion: Continuation): Unit =
when (this) {
- CoroutineStart.DEFAULT -> block.startCoroutineCancellable(receiver, completion)
- CoroutineStart.ATOMIC -> block.startCoroutine(receiver, completion)
- CoroutineStart.UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
- CoroutineStart.LAZY -> Unit // will start lazily
+ DEFAULT -> block.startCoroutineCancellable(receiver, completion)
+ ATOMIC -> block.startCoroutine(receiver, completion)
+ UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
+ LAZY -> Unit // will start lazily
}
/**
diff --git a/kotlinx-coroutines-core/common/src/Debug.common.kt b/kotlinx-coroutines-core/common/src/Debug.common.kt
index 013b983a74..949b05c63f 100644
--- a/kotlinx-coroutines-core/common/src/Debug.common.kt
+++ b/kotlinx-coroutines-core/common/src/Debug.common.kt
@@ -27,7 +27,7 @@ internal expect fun assert(value: () -> Boolean)
* Copy mechanism is used only on JVM, but it might be convenient to implement it in common exceptions,
* so on JVM their stacktraces will be properly recovered.
*/
-@ExperimentalCoroutinesApi
+@ExperimentalCoroutinesApi // Since 1.2.0, no ETA on stability
public interface CopyableThrowable where T : Throwable, T : CopyableThrowable {
/**
diff --git a/kotlinx-coroutines-core/common/src/Deferred.kt b/kotlinx-coroutines-core/common/src/Deferred.kt
index f05abbdb43..ff996756a3 100644
--- a/kotlinx-coroutines-core/common/src/Deferred.kt
+++ b/kotlinx-coroutines-core/common/src/Deferred.kt
@@ -30,6 +30,9 @@ import kotlinx.coroutines.selects.*
*
* All functions on this interface and on all interfaces derived from it are **thread-safe** and can
* be safely invoked from concurrent coroutines without external synchronization.
+ *
+ * **`Deferred` interface and all its derived interfaces are not stable for inheritance in 3rd party libraries**,
+ * as new methods might be added to this interface in the future, but is stable for use.
*/
public interface Deferred : Job {
@@ -40,6 +43,8 @@ public interface Deferred : Job {
* This suspending function is cancellable.
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
* immediately resumes with [CancellationException].
+ * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was
+ * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details.
*
* This function can be used in [select] invocation with [onAwait] clause.
* Use [isCompleted] to check for completion of this deferred value without waiting.
diff --git a/kotlinx-coroutines-core/common/src/Delay.kt b/kotlinx-coroutines-core/common/src/Delay.kt
index cc205ecaf0..aae623d5df 100644
--- a/kotlinx-coroutines-core/common/src/Delay.kt
+++ b/kotlinx-coroutines-core/common/src/Delay.kt
@@ -21,11 +21,14 @@ import kotlin.time.*
public interface Delay {
/**
* Delays coroutine for a given time without blocking a thread and resumes it after a specified time.
+ *
* This suspending function is cancellable.
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
* immediately resumes with [CancellationException].
+ * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was
+ * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details.
*/
- suspend fun delay(time: Long) {
+ public suspend fun delay(time: Long) {
if (time <= 0) return // don't delay
return suspendCancellableCoroutine { scheduleResumeAfterDelay(time, it) }
}
@@ -45,7 +48,7 @@ public interface Delay {
* with(continuation) { resumeUndispatchedWith(Unit) }
* ```
*/
- fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation)
+ public fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation)
/**
* Schedules invocation of a specified [block] after a specified delay [timeMillis].
@@ -54,15 +57,56 @@ public interface Delay {
*
* This implementation uses a built-in single-threaded scheduled executor service.
*/
- fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle =
- DefaultDelay.invokeOnTimeout(timeMillis, block)
+ public fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
+ DefaultDelay.invokeOnTimeout(timeMillis, block, context)
}
+/**
+ * Suspends until cancellation, in which case it will throw a [CancellationException].
+ *
+ * This function returns [Nothing], so it can be used in any coroutine,
+ * regardless of the required return type.
+ *
+ * Usage example in callback adapting code:
+ *
+ * ```kotlin
+ * fun currentTemperature(): Flow = callbackFlow {
+ * val callback = SensorCallback { degreesCelsius: Double ->
+ * trySend(Temperature.celsius(degreesCelsius))
+ * }
+ * try {
+ * registerSensorCallback(callback)
+ * awaitCancellation() // Suspends to keep getting updates until cancellation.
+ * } finally {
+ * unregisterSensorCallback(callback)
+ * }
+ * }
+ * ```
+ *
+ * Usage example in (non declarative) UI code:
+ *
+ * ```kotlin
+ * suspend fun showStuffUntilCancelled(content: Stuff): Nothing {
+ * someSubView.text = content.title
+ * anotherSubView.text = content.description
+ * someView.visibleInScope {
+ * awaitCancellation() // Suspends so the view stays visible.
+ * }
+ * }
+ * ```
+ */
+public suspend fun awaitCancellation(): Nothing = suspendCancellableCoroutine {}
+
/**
* Delays coroutine for a given time without blocking a thread and resumes it after a specified time.
+ *
* This suspending function is cancellable.
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
* immediately resumes with [CancellationException].
+ * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was
+ * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details.
+ *
+ * If you want to delay forever (until cancellation), consider using [awaitCancellation] instead.
*
* Note that delay can be used in [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
*
@@ -72,22 +116,30 @@ public interface Delay {
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation ->
- cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
+ // if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
+ if (timeMillis < Long.MAX_VALUE) {
+ cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
+ }
}
}
/**
* Delays coroutine for a given [duration] without blocking a thread and resumes it after the specified time.
+ *
* This suspending function is cancellable.
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
* immediately resumes with [CancellationException].
+ * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was
+ * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details.
+ *
+ * If you want to delay forever (until cancellation), consider using [awaitCancellation] instead.
*
* Note that delay can be used in [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
*
* Implementation note: how exactly time is tracked is an implementation detail of [CoroutineDispatcher] in the context.
*/
@ExperimentalTime
-public suspend fun delay(duration: Duration) = delay(duration.toDelayMillis())
+public suspend fun delay(duration: Duration): Unit = delay(duration.toDelayMillis())
/** Returns [Delay] implementation of the given context */
internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay
diff --git a/kotlinx-coroutines-core/common/src/EventLoop.common.kt b/kotlinx-coroutines-core/common/src/EventLoop.common.kt
index ba331e20df..69ea9fe312 100644
--- a/kotlinx-coroutines-core/common/src/EventLoop.common.kt
+++ b/kotlinx-coroutines-core/common/src/EventLoop.common.kt
@@ -52,7 +52,7 @@ internal abstract class EventLoop : CoroutineDispatcher() {
*/
public open fun processNextEvent(): Long {
if (!processUnconfinedEvent()) return Long.MAX_VALUE
- return nextTime
+ return 0
}
protected open val isEmpty: Boolean get() = isUnconfinedQueueEmpty
@@ -251,7 +251,7 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay {
override fun processNextEvent(): Long {
// unconfined events take priority
- if (processUnconfinedEvent()) return nextTime
+ if (processUnconfinedEvent()) return 0
// queue all delayed tasks that are due to be executed
val delayed = _delayed.value
if (delayed != null && !delayed.isEmpty) {
@@ -269,7 +269,11 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay {
}
}
// then process one event from queue
- dequeue()?.run()
+ val task = dequeue()
+ if (task != null) {
+ task.run()
+ return 0
+ }
return nextTime
}
diff --git a/kotlinx-coroutines-core/common/src/Job.kt b/kotlinx-coroutines-core/common/src/Job.kt
index 4d4e37ee25..2e05635a29 100644
--- a/kotlinx-coroutines-core/common/src/Job.kt
+++ b/kotlinx-coroutines-core/common/src/Job.kt
@@ -19,17 +19,22 @@ import kotlin.jvm.*
* culminates in its completion.
*
* Jobs can be arranged into parent-child hierarchies where cancellation
- * of a parent leads to immediate cancellation of all its [children]. Failure or cancellation of a child
- * with an exception other than [CancellationException] immediately cancels its parent. This way, a parent
- * can [cancel] its own children (including all their children recursively) without cancelling itself.
+ * of a parent leads to immediate cancellation of all its [children] recursively.
+ * Failure of a child with an exception other than [CancellationException] immediately cancels its parent and,
+ * consequently, all its other children. This behavior can be customized using [SupervisorJob].
*
- * The most basic instances of [Job] are created with [launch][CoroutineScope.launch] coroutine builder or with a
- * `Job()` factory function. By default, a failure of any of the job's children leads to an immediate failure
- * of its parent and cancellation of the rest of its children. This behavior can be customized using [SupervisorJob].
+ * The most basic instances of `Job` interface are created like this:
*
- * Conceptually, an execution of the job does not produce a result value. Jobs are launched solely for their
+ * * **Coroutine job** is created with [launch][CoroutineScope.launch] coroutine builder.
+ * It runs a specified block of code and completes on completion of this block.
+ * * **[CompletableJob]** is created with a `Job()` factory function.
+ * It is completed by calling [CompletableJob.complete].
+ *
+ * Conceptually, an execution of a job does not produce a result value. Jobs are launched solely for their
* side-effects. See [Deferred] interface for a job that produces a result.
*
+ * ### Job states
+ *
* A job has the following states:
*
* | **State** | [isActive] | [isCompleted] | [isCancelled] |
@@ -41,13 +46,23 @@ import kotlin.jvm.*
* | _Cancelled_ (final state) | `false` | `true` | `true` |
* | _Completed_ (final state) | `false` | `true` | `false` |
*
- * Usually, a job is created in _active_ state (it is created and started). However, coroutine builders
- * that provide an optional `start` parameter create a coroutine in _new_ state when this parameter is set to
+ * Usually, a job is created in the _active_ state (it is created and started). However, coroutine builders
+ * that provide an optional `start` parameter create a coroutine in the _new_ state when this parameter is set to
* [CoroutineStart.LAZY]. Such a job can be made _active_ by invoking [start] or [join].
*
- * A job is _active_ while the coroutine is working. Failure of the job with exception makes it _cancelling_.
+ * A job is _active_ while the coroutine is working or until [CompletableJob] is completed,
+ * or until it fails or cancelled.
+ *
+ * Failure of an _active_ job with an exception makes it _cancelling_.
* A job can be cancelled at any time with [cancel] function that forces it to transition to
- * _cancelling_ state immediately. The job becomes _cancelled_ when it finishes executing its work.
+ * the _cancelling_ state immediately. The job becomes _cancelled_ when it finishes executing its work and
+ * all its children complete.
+ *
+ * Completion of an _active_ coroutine's body or a call to [CompletableJob.complete] transitions the job to
+ * the _completing_ state. It waits in the _completing_ state for all its children to complete before
+ * transitioning to the _completed_ state.
+ * Note that _completing_ state is purely internal to the job. For an outside observer a _completing_ job is still
+ * active, while internally it is waiting for its children.
*
* ```
* wait children
@@ -67,19 +82,32 @@ import kotlin.jvm.*
* [coroutineContext](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/coroutine-context.html)
* represents the coroutine itself.
*
- * A job can have a _parent_ job. A job with a parent is cancelled when its parent is cancelled.
- * Parent job waits in _completing_ or _cancelling_ state for all its children to complete before finishing.
- * Note that _completing_ state is purely internal to the job. For an outside observer a _completing_ job is still
- * active, while internally it is waiting for its children.
+ * ### Cancellation cause
+ *
+ * A coroutine job is said to _complete exceptionally_ when its body throws an exception;
+ * a [CompletableJob] is completed exceptionally by calling [CompletableJob.completeExceptionally].
+ * An exceptionally completed job is cancelled and the corresponding exception becomes the _cancellation cause_ of the job.
*
- * Normal cancellation of a job is distinguished from its failure by the type of its cancellation exception cause.
- * If the cause of cancellation is [CancellationException], then the job is considered to be _cancelled normally_.
- * This usually happens when [cancel] is invoked without additional parameters. If the cause of cancellation is
- * a different exception, then the job is considered to have _failed_. This usually happens when the code of the job
- * encounters some problem and throws an exception.
+ * Normal cancellation of a job is distinguished from its failure by the type of this exception that caused its cancellation.
+ * A coroutine that threw [CancellationException] is considered to be _cancelled normally_.
+ * If a cancellation cause is a different exception type, then the job is considered to have _failed_.
+ * When a job has _failed_, then its parent gets cancelled with the exception of the same type,
+ * thus ensuring transparency in delegating parts of the job to its children.
+ *
+ * Note, that [cancel] function on a job only accepts [CancellationException] as a cancellation cause, thus
+ * calling [cancel] always results in a normal cancellation of a job, which does not lead to cancellation
+ * of its parent. This way, a parent can [cancel] its own children (cancelling all their children recursively, too)
+ * without cancelling itself.
+ *
+ * ### Concurrency and synchronization
*
* All functions on this interface and on all interfaces derived from it are **thread-safe** and can
* be safely invoked from concurrent coroutines without external synchronization.
+ *
+ * ### Not stable for inheritance
+ *
+ * **`Job` interface and all its derived interfaces are not stable for inheritance in 3rd party libraries**,
+ * as new methods might be added to this interface in the future, but is stable for use.
*/
public interface Job : CoroutineContext.Element {
/**
@@ -167,7 +195,7 @@ public interface Job : CoroutineContext.Element {
* @suppress This method implements old version of JVM ABI. Use [cancel].
*/
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x")
- public fun cancel() = cancel(null)
+ public fun cancel(): Unit = cancel(null)
/**
* @suppress This method has bad semantics when cause is not a [CancellationException]. Use [cancel].
@@ -337,7 +365,7 @@ public interface Job : CoroutineContext.Element {
"Job is a coroutine context element and `+` is a set-sum operator for coroutine contexts. " +
"The job to the right of `+` just replaces the job the left of `+`.",
level = DeprecationLevel.ERROR)
- public operator fun plus(other: Job) = other
+ public operator fun plus(other: Job): Job = other
}
/**
@@ -382,7 +410,7 @@ public interface DisposableHandle {
*/
@Suppress("FunctionName")
@InternalCoroutinesApi
-public inline fun DisposableHandle(crossinline block: () -> Unit) =
+public inline fun DisposableHandle(crossinline block: () -> Unit): DisposableHandle =
object : DisposableHandle {
override fun dispose() {
block()
@@ -496,7 +524,7 @@ public fun Job.cancelChildren(cause: CancellationException? = null) {
* @suppress This method implements old version of JVM ABI. Use [cancel].
*/
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x")
-public fun Job.cancelChildren() = cancelChildren(null)
+public fun Job.cancelChildren(): Unit = cancelChildren(null)
/**
* @suppress This method has bad semantics when cause is not a [CancellationException]. Use [Job.cancelChildren].
@@ -539,7 +567,7 @@ public fun CoroutineContext.cancel(cause: CancellationException? = null) {
* @suppress This method implements old version of JVM ABI. Use [CoroutineContext.cancel].
*/
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x")
-public fun CoroutineContext.cancel() = cancel(null)
+public fun CoroutineContext.cancel(): Unit = cancel(null)
/**
* Ensures that current job is [active][Job.isActive].
@@ -559,10 +587,10 @@ public fun Job.ensureActive(): Unit {
/**
* Ensures that job in the current context is [active][Job.isActive].
- * Throws [IllegalStateException] if the context does not have a job in it.
*
* If the job is no longer active, throws [CancellationException].
* If the job was cancelled, thrown exception contains the original cancellation cause.
+ * This function does not do anything if there is no [Job] in the context, since such a coroutine cannot be cancelled.
*
* This method is a drop-in replacement for the following code, but with more precise exception:
* ```
@@ -571,9 +599,8 @@ public fun Job.ensureActive(): Unit {
* }
* ```
*/
-public fun CoroutineContext.ensureActive(): Unit {
- val job = get(Job) ?: error("Context cannot be checked for liveness because it does not have a job: $this")
- job.ensureActive()
+public fun CoroutineContext.ensureActive() {
+ get(Job)?.ensureActive()
}
/**
@@ -605,7 +632,16 @@ public fun CoroutineContext.cancelChildren(cause: CancellationException? = null)
* @suppress This method implements old version of JVM ABI. Use [CoroutineContext.cancelChildren].
*/
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x")
-public fun CoroutineContext.cancelChildren() = cancelChildren(null)
+public fun CoroutineContext.cancelChildren(): Unit = cancelChildren(null)
+
+/**
+ * Retrieves the current [Job] instance from the given [CoroutineContext] or
+ * throws [IllegalStateException] if no job is present in the context.
+ *
+ * This method is a short-cut for `coroutineContext[Job]!!` and should be used only when it is known in advance that
+ * the context does have instance of the job in it.
+ */
+public val CoroutineContext.job: Job get() = get(Job) ?: error("Current context doesn't contain Job in it: $this")
/**
* @suppress This method has bad semantics when cause is not a [CancellationException]. Use [CoroutineContext.cancelChildren].
diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt
index e52aaeaa8e..020d00a32c 100644
--- a/kotlinx-coroutines-core/common/src/JobSupport.kt
+++ b/kotlinx-coroutines-core/common/src/JobSupport.kt
@@ -652,7 +652,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
* Makes this [Job] cancelled with a specified [cause].
* It is used in [AbstractCoroutine]-derived classes when there is an internal failure.
*/
- public fun cancelCoroutine(cause: Throwable?) = cancelImpl(cause)
+ public fun cancelCoroutine(cause: Throwable?): Boolean = cancelImpl(cause)
// cause is Throwable or ParentJob when cancelChild was invoked
// returns true is exception was handled, false otherwise
diff --git a/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt
index 3f2ddcd69f..daba38f0fd 100644
--- a/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt
+++ b/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt
@@ -43,4 +43,27 @@ public abstract class MainCoroutineDispatcher : CoroutineDispatcher() {
* [Dispatchers.Main] supports immediate execution for Android, JavaFx and Swing platforms.
*/
public abstract val immediate: MainCoroutineDispatcher
+
+ /**
+ * Returns a name of this main dispatcher for debugging purposes. This implementation returns
+ * `Dispatchers.Main` or `Dispatchers.Main.immediate` if it is the same as the corresponding
+ * reference in [Dispatchers] or a short class-name representation with address otherwise.
+ */
+ override fun toString(): String = toStringInternalImpl() ?: "$classSimpleName@$hexAddress"
+
+ /**
+ * Internal method for more specific [toString] implementations. It returns non-null
+ * string if this dispatcher is set in the platform as the main one.
+ * @suppress
+ */
+ @InternalCoroutinesApi
+ protected fun toStringInternalImpl(): String? {
+ val main = Dispatchers.Main
+ if (this === main) return "Dispatchers.Main"
+ val immediate =
+ try { main.immediate }
+ catch (e: UnsupportedOperationException) { null }
+ if (this === immediate) return "Dispatchers.Main.immediate"
+ return null
+ }
}
diff --git a/kotlinx-coroutines-core/common/src/Supervisor.kt b/kotlinx-coroutines-core/common/src/Supervisor.kt
index 1991119053..542e4fef48 100644
--- a/kotlinx-coroutines-core/common/src/Supervisor.kt
+++ b/kotlinx-coroutines-core/common/src/Supervisor.kt
@@ -1,13 +1,14 @@
/*
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
-
+@file:OptIn(ExperimentalContracts::class)
@file:Suppress("DEPRECATION_ERROR")
package kotlinx.coroutines
import kotlinx.coroutines.internal.*
import kotlinx.coroutines.intrinsics.*
+import kotlin.contracts.*
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*
import kotlin.jvm.*
@@ -47,11 +48,15 @@ public fun SupervisorJob0(parent: Job? = null) : Job = SupervisorJob(parent)
* A failure of the scope itself (exception thrown in the [block] or cancellation) fails the scope with all its children,
* but does not cancel parent job.
*/
-public suspend fun supervisorScope(block: suspend CoroutineScope.() -> R): R =
- suspendCoroutineUninterceptedOrReturn { uCont ->
+public suspend fun supervisorScope(block: suspend CoroutineScope.() -> R): R {
+ contract {
+ callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+ }
+ return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = SupervisorCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
+}
private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
override fun childCancelled(cause: Throwable): Boolean = false
diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt
index 87fe733773..4bfff118e8 100644
--- a/kotlinx-coroutines-core/common/src/Timeout.kt
+++ b/kotlinx-coroutines-core/common/src/Timeout.kt
@@ -1,12 +1,14 @@
/*
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
+@file:OptIn(ExperimentalContracts::class)
package kotlinx.coroutines
import kotlinx.coroutines.internal.*
import kotlinx.coroutines.intrinsics.*
import kotlinx.coroutines.selects.*
+import kotlin.contracts.*
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*
import kotlin.jvm.*
@@ -22,11 +24,21 @@ import kotlin.time.*
* The sibling function that does not throw an exception on timeout is [withTimeoutOrNull].
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
*
- * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
+ * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
+ * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some
+ * resource inside the [block] that needs closing or release outside of the block.
+ * See the
+ * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
+ * section of the coroutines guide for details.
+ *
+ * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
*
* @param timeMillis timeout time in milliseconds.
*/
public suspend fun withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T {
+ contract {
+ callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+ }
if (timeMillis <= 0L) throw TimeoutCancellationException("Timed out immediately")
return suspendCoroutineUninterceptedOrReturn { uCont ->
setupTimeout(TimeoutCoroutine(timeMillis, uCont), block)
@@ -43,11 +55,22 @@ public suspend fun withTimeout(timeMillis: Long, block: suspend CoroutineSco
* The sibling function that does not throw an exception on timeout is [withTimeoutOrNull].
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
*
- * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
+ * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
+ * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some
+ * resource inside the [block] that needs closing or release outside of the block.
+ * See the
+ * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
+ * section of the coroutines guide for details.
+ *
+ * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
*/
@ExperimentalTime
-public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T =
- withTimeout(timeout.toDelayMillis(), block)
+public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T {
+ contract {
+ callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+ }
+ return withTimeout(timeout.toDelayMillis(), block)
+}
/**
* Runs a given suspending block of code inside a coroutine with a specified [timeout][timeMillis] and returns
@@ -59,7 +82,14 @@ public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineSc
* The sibling function that throws an exception on timeout is [withTimeout].
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
*
- * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
+ * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
+ * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some
+ * resource inside the [block] that needs closing or release outside of the block.
+ * See the
+ * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
+ * section of the coroutines guide for details.
+ *
+ * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
*
* @param timeMillis timeout time in milliseconds.
*/
@@ -92,7 +122,14 @@ public suspend fun withTimeoutOrNull(timeMillis: Long, block: suspend Corout
* The sibling function that throws an exception on timeout is [withTimeout].
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
*
- * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
+ * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
+ * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some
+ * resource inside the [block] that needs closing or release outside of the block.
+ * See the
+ * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
+ * section of the coroutines guide for details.
+ *
+ * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
*/
@ExperimentalTime
public suspend fun withTimeoutOrNull(timeout: Duration, block: suspend CoroutineScope.() -> T): T? =
@@ -105,7 +142,7 @@ private fun setupTimeout(
// schedule cancellation of this coroutine on time
val cont = coroutine.uCont
val context = cont.context
- coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine))
+ coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine, coroutine.context))
// restart the block using a new coroutine with a new job,
// however, start it undispatched, because we already are in the proper context
return coroutine.startUndispatchedOrReturnIgnoreTimeout(coroutine, block)
diff --git a/kotlinx-coroutines-core/common/src/Unconfined.kt b/kotlinx-coroutines-core/common/src/Unconfined.kt
index ce03a28765..a0997a51d3 100644
--- a/kotlinx-coroutines-core/common/src/Unconfined.kt
+++ b/kotlinx-coroutines-core/common/src/Unconfined.kt
@@ -26,7 +26,7 @@ internal object Unconfined : CoroutineDispatcher() {
"isDispatchNeeded and dispatch calls.")
}
- override fun toString(): String = "Unconfined"
+ override fun toString(): String = "Dispatchers.Unconfined"
}
/**
diff --git a/kotlinx-coroutines-core/common/src/Yield.kt b/kotlinx-coroutines-core/common/src/Yield.kt
index e0af04ddb7..0d8bd3bc2f 100644
--- a/kotlinx-coroutines-core/common/src/Yield.kt
+++ b/kotlinx-coroutines-core/common/src/Yield.kt
@@ -4,6 +4,7 @@
package kotlinx.coroutines
+import kotlinx.coroutines.internal.*
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*
@@ -13,6 +14,8 @@ import kotlin.coroutines.intrinsics.*
* This suspending function is cancellable.
* If the [Job] of the current coroutine is cancelled or completed when this suspending function is invoked or while
* this function is waiting for dispatch, it resumes with a [CancellationException].
+ * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was
+ * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details.
*
* **Note**: This function always [checks for cancellation][ensureActive] even when it does not suspend.
*
diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt
index 8d078e49ca..8edd2b310c 100644
--- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt
+++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt
@@ -16,7 +16,9 @@ import kotlin.native.concurrent.*
/**
* Abstract send channel. It is a base class for all send channel implementations.
*/
-internal abstract class AbstractSendChannel : SendChannel {
+internal abstract class AbstractSendChannel(
+ @JvmField protected val onUndeliveredElement: OnUndeliveredElement?
+) : SendChannel {
/** @suppress **This is unstable API and it is subject to change.** */
protected val queue = LockFreeLinkedListHead()
@@ -122,22 +124,6 @@ internal abstract class AbstractSendChannel : SendChannel {
}
}
- /**
- * @suppress **This is unstable API and it is subject to change.**
- */
- protected fun describeSendConflated(element: E): AddLastDesc<*> = SendConflatedDesc(queue, element)
-
- private class SendConflatedDesc(
- queue: LockFreeLinkedListHead,
- element: E
- ) : SendBufferedDesc(queue, element) {
- override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) {
- super.finishOnSuccess(affected, next)
- // remove previous SendBuffered
- (affected as? SendBuffered<*>)?.remove()
- }
- }
-
// ------ SendChannel ------
public final override val isClosedForSend: Boolean get() = closedForSend != null
@@ -151,14 +137,6 @@ internal abstract class AbstractSendChannel : SendChannel {
return sendSuspend(element)
}
- internal suspend fun sendFair(element: E) {
- if (offerInternal(element) === OFFER_SUCCESS) {
- yield() // Works only on fast path to properly work in sequential use-cases
- return
- }
- return sendSuspend(element)
- }
-
public final override fun offer(element: E): Boolean {
val result = offerInternal(element)
return when {
@@ -167,24 +145,34 @@ internal abstract class AbstractSendChannel : SendChannel {
// We should check for closed token on offer as well, otherwise offer won't be linearizable
// in the face of concurrent close()
// See https://github.com/Kotlin/kotlinx.coroutines/issues/359
- throw recoverStackTrace(helpCloseAndGetSendException(closedForSend ?: return false))
+ throw recoverStackTrace(helpCloseAndGetSendException(element, closedForSend ?: return false))
+ }
+ result is Closed<*> -> {
+ throw recoverStackTrace(helpCloseAndGetSendException(element, result))
}
- result is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(result))
else -> error("offerInternal returned $result")
}
}
- private fun helpCloseAndGetSendException(closed: Closed<*>): Throwable {
+ private fun helpCloseAndGetSendException(element: E, closed: Closed<*>): Throwable {
// To ensure linearizablity we must ALWAYS help close the channel when we observe that it was closed
// See https://github.com/Kotlin/kotlinx.coroutines/issues/1419
helpClose(closed)
+ // Element was not delivered -> cals onUndeliveredElement
+ onUndeliveredElement?.callUndeliveredElementCatchingException(element)?.let {
+ // If it crashes, add send exception as suppressed for better diagnostics
+ it.addSuppressed(closed.sendException)
+ throw it
+ }
return closed.sendException
}
- private suspend fun sendSuspend(element: E): Unit = suspendAtomicCancellableCoroutineReusable sc@ { cont ->
+ private suspend fun sendSuspend(element: E): Unit = suspendCancellableCoroutineReusable sc@ { cont ->
loop@ while (true) {
if (isFullImpl) {
- val send = SendElement(element, cont)
+ val send = if (onUndeliveredElement == null)
+ SendElement(element, cont) else
+ SendElementWithUndeliveredHandler(element, cont, onUndeliveredElement)
val enqueueResult = enqueueSend(send)
when {
enqueueResult == null -> { // enqueued successfully
@@ -192,7 +180,7 @@ internal abstract class AbstractSendChannel : SendChannel {
return@sc
}
enqueueResult is Closed<*> -> {
- cont.helpCloseAndResumeWithSendException(enqueueResult)
+ cont.helpCloseAndResumeWithSendException(element, enqueueResult)
return@sc
}
enqueueResult === ENQUEUE_FAILED -> {} // try to offer instead
@@ -209,7 +197,7 @@ internal abstract class AbstractSendChannel : SendChannel {
}
offerResult === OFFER_FAILED -> continue@loop
offerResult is Closed<*> -> {
- cont.helpCloseAndResumeWithSendException(offerResult)
+ cont.helpCloseAndResumeWithSendException(element, offerResult)
return@sc
}
else -> error("offerInternal returned $offerResult")
@@ -217,9 +205,15 @@ internal abstract class AbstractSendChannel : SendChannel {
}
}
- private fun Continuation<*>.helpCloseAndResumeWithSendException(closed: Closed<*>) {
+ private fun Continuation<*>.helpCloseAndResumeWithSendException(element: E, closed: Closed<*>) {
helpClose(closed)
- resumeWithException(closed.sendException)
+ val sendException = closed.sendException
+ onUndeliveredElement?.callUndeliveredElementCatchingException(element)?.let {
+ it.addSuppressed(sendException)
+ resumeWithException(it)
+ return
+ }
+ resumeWithException(sendException)
}
/**
@@ -391,7 +385,7 @@ internal abstract class AbstractSendChannel : SendChannel {
select.disposeOnSelect(node)
return
}
- enqueueResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(enqueueResult))
+ enqueueResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(element, enqueueResult))
enqueueResult === ENQUEUE_FAILED -> {} // try to offer
enqueueResult is Receive<*> -> {} // try to offer
else -> error("enqueueSend returned $enqueueResult ")
@@ -407,7 +401,7 @@ internal abstract class AbstractSendChannel : SendChannel {
block.startCoroutineUnintercepted(receiver = this, completion = select.completion)
return
}
- offerResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(offerResult))
+ offerResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(element, offerResult))
else -> error("offerSelectInternal returned $offerResult")
}
}
@@ -447,7 +441,7 @@ internal abstract class AbstractSendChannel : SendChannel {
// ------ private ------
private class SendSelect(
- override val pollResult: Any?,
+ override val pollResult: E, // E | Closed - the result pollInternal returns when it rendezvous with this node
@JvmField val channel: AbstractSendChannel,
@JvmField val select: SelectInstance,
@JvmField val block: suspend (SendChannel) -> R
@@ -456,11 +450,13 @@ internal abstract class AbstractSendChannel : SendChannel {
select.trySelectOther(otherOp) as Symbol? // must return symbol
override fun completeResumeSend() {
- block.startCoroutine(receiver = channel, completion = select.completion)
+ block.startCoroutineCancellable(receiver = channel, completion = select.completion)
}
override fun dispose() { // invoked on select completion
- remove()
+ if (!remove()) return
+ // if the node was successfully removed (meaning it was added but was not received) then element not delivered
+ undeliveredElement()
}
override fun resumeSendClosed(closed: Closed<*>) {
@@ -468,6 +464,10 @@ internal abstract class AbstractSendChannel : SendChannel {
select.resumeSelectWithException(closed.sendException)
}
+ override fun undeliveredElement() {
+ channel.onUndeliveredElement?.callUndeliveredElement(pollResult, select.completion.context)
+ }
+
override fun toString(): String = "SendSelect@$hexAddress($pollResult)[$channel, $select]"
}
@@ -485,7 +485,9 @@ internal abstract class AbstractSendChannel : SendChannel {
/**
* Abstract send/receive channel. It is a base class for all channel implementations.
*/
-internal abstract class AbstractChannel : AbstractSendChannel(), Channel {
+internal abstract class AbstractChannel(
+ onUndeliveredElement: OnUndeliveredElement?
+) : AbstractSendChannel(onUndeliveredElement), Channel {
// ------ extension points for buffered channels ------
/**
@@ -517,6 +519,8 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel : AbstractSendChannel(), Channel receiveSuspend(receiveMode: Int): R = suspendAtomicCancellableCoroutineReusable sc@ { cont ->
- val receive = ReceiveElement(cont as CancellableContinuation, receiveMode)
+ private suspend fun receiveSuspend(receiveMode: Int): R = suspendCancellableCoroutineReusable sc@ { cont ->
+ val receive = if (onUndeliveredElement == null)
+ ReceiveElement(cont as CancellableContinuation, receiveMode) else
+ ReceiveElementWithUndeliveredHandler(cont as CancellableContinuation, receiveMode, onUndeliveredElement)
while (true) {
if (enqueueReceive(receive)) {
removeReceiveOnCancel(cont, receive)
@@ -577,7 +583,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel : AbstractSendChannel(), Channel
@@ -801,7 +812,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel, receive: Receive<*>) =
cont.invokeOnCancellation(handler = RemoveReceiveOnCancel(receive).asHandler)
- private inner class RemoveReceiveOnCancel(private val receive: Receive<*>) : CancelHandler() {
+ private inner class RemoveReceiveOnCancel(private val receive: Receive<*>) : BeforeResumeCancelHandler() {
override fun invoke(cause: Throwable?) {
if (receive.remove())
onReceiveDequeued()
@@ -809,7 +820,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel