Document coroutines codegen: interception
This commit is contained in:
committed by
Ilmir Usmanov
parent
8b604b8ce1
commit
bfd0291572
+124
-1
@@ -1060,4 +1060,127 @@ However, this includes not only recursive lambdas. We can pass the lambda to a t
|
|||||||
case, the continuation object is the same, and we have the same problems as if there was a recursion.
|
case, the continuation object is the same, and we have the same problems as if there was a recursion.
|
||||||
|
|
||||||
Of course, in JVM_IR, we do not have a `create` function in case when the lambda has more than one parameter, `invoke` creates a new
|
Of course, in JVM_IR, we do not have a `create` function in case when the lambda has more than one parameter, `invoke` creates a new
|
||||||
instance of the lambda with copies of all captured variables and then puts the parameters of the lambda to fields.
|
instance of the lambda with copies of all captured variables and then puts the parameters of the lambda to fields.
|
||||||
|
|
||||||
|
#### Interception
|
||||||
|
|
||||||
|
After all this boring theory, we can finally turn our `async` example from the previous section into a multithreaded one. In all previous
|
||||||
|
examples I used `EmptyCoroutineContext` as `context` for root continuations. `CoroutineContext`, the type of `context` property, is
|
||||||
|
essentially a hash map from `CoroutineContext.Key` to `CoroutineContext.Element`. A programmer can store coroutine-local information in it,
|
||||||
|
and here 'coroutine' is used in a broad sense to represent a lightweight thread, not just a suspend function or a suspend lambda. So, one
|
||||||
|
can view `context` as a replacement of `ThreadLocal`. To access it, the user should use `coroutineContext` intrinsic. Even a single context
|
||||||
|
element is a context itself, so it forms a tree. The fact that one element of the context is context itself comes in handy when we need to
|
||||||
|
move a coroutine from one thread to another, i.e., intercept it. In order to do that, we need to provide the key and the element to the
|
||||||
|
context. There is a special interface `ContinuationInterceptor`, which overrides `CoroutineContext.Element` and has a property `key`.
|
||||||
|
Let us create one:
|
||||||
|
```kotlin
|
||||||
|
object SingleThreadedInterceptor: ContinuationInterceptor {
|
||||||
|
override val key = ContinuationInterceptor.Key
|
||||||
|
|
||||||
|
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
|
||||||
|
SingleThreadedContinuation(continuation)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In its method `interceptContinuation` we simply wrap provided continuation with a new one, and in this continuation we can run the coroutine
|
||||||
|
on a different thread:
|
||||||
|
```kotlin
|
||||||
|
class SingleThreadedContinuation<T>(val c: Continuation<T>): Continuation<T> {
|
||||||
|
override val context: CoroutineContext
|
||||||
|
get() = c.context
|
||||||
|
|
||||||
|
override fun resumeWith(result: Result<T>) {
|
||||||
|
thread {
|
||||||
|
c.resumeWith(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Inside the `resumeWith` function, as one can see, we simply resume the continuation on another thread.
|
||||||
|
|
||||||
|
Note that we pass the `context` of provided continuation as our own, so our continuation inherits it from the wrapped one. That is not
|
||||||
|
required, but since `context` is a replacement for `ThreadLocal`, we should keep it and. All we are allowed to do is add additional
|
||||||
|
infrastructural information, like `ContinuationInterceptor`, but we can never remove anything added by the user.
|
||||||
|
|
||||||
|
It is important to note that the `key` property should be constant. Otherwise, `get` on this key will return null, and there will be no
|
||||||
|
interception.
|
||||||
|
|
||||||
|
Now, if we change `AsyncContinuation`, `async` function and `main` to use the interceptor:
|
||||||
|
```kotlin
|
||||||
|
class AsyncContinuation<T>(override val context: CoroutineContext): Continuation<T> {
|
||||||
|
var result: T? = null
|
||||||
|
var awaiting: Continuation<T>? = null
|
||||||
|
|
||||||
|
override fun resumeWith(result: Result<T>) {
|
||||||
|
if (awaiting != null) {
|
||||||
|
awaiting?.resumeWith(result)
|
||||||
|
} else {
|
||||||
|
this.result = result.getOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> async(context: CoroutineContext = EmptyCoroutineContext, c: suspend () -> T): Async<T> {
|
||||||
|
val ac = AsyncContinuation<T>(context)
|
||||||
|
c.startCoroutine(ac)
|
||||||
|
return Async(ac)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
var c: Continuation<String>? = null
|
||||||
|
builder {
|
||||||
|
val async = async(SingleThreadedIntercepted) {
|
||||||
|
println("Async in thread ${Thread.currentThread().id}")
|
||||||
|
suspendCoroutine<String> { c = it }
|
||||||
|
}
|
||||||
|
println("Await in thread ${Thread.currentThread().id}")
|
||||||
|
println(async.await())
|
||||||
|
}
|
||||||
|
c?.resume("OK")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
and when we run the program, we get something like
|
||||||
|
```text
|
||||||
|
Async in thread 11
|
||||||
|
Await in thread 1
|
||||||
|
OK
|
||||||
|
```
|
||||||
|
as expected.
|
||||||
|
|
||||||
|
But what part of coroutine machinery calls `interceptContinuation` function of the interceptor? The function wraps the continuation, but
|
||||||
|
who calls the function? Well, `intercepted` does. If we rewrite
|
||||||
|
`async` as
|
||||||
|
```kotlin
|
||||||
|
fun <T> async(context: CoroutineContext = EmptyCoroutineContext, c: suspend () -> T): Async<T> {
|
||||||
|
val ac = AsyncContinuation<T>(context)
|
||||||
|
c.createCoroutineUnintercepted(ac)
|
||||||
|
// .intercepted()
|
||||||
|
.resume(Unit)
|
||||||
|
return Async(ac)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
(note, that I commented `intercepted` call out) and then run the example, we get
|
||||||
|
```text
|
||||||
|
Async in thread 1
|
||||||
|
Await in thread 1
|
||||||
|
OK
|
||||||
|
```
|
||||||
|
since without interception, we do not wrap the continuation to run the coroutine on another thread.
|
||||||
|
|
||||||
|
But how `intercepted` does that? Well, `intercepted`, after some indirections, does the following:
|
||||||
|
```kotlin
|
||||||
|
context[ContinuationInterceptor]?.interceptContinuation(this)
|
||||||
|
```
|
||||||
|
remember, `CoroutineContext.Element` is itself a `CoroutineContext` with a single element, which returns itself on `get` if its key is the
|
||||||
|
same as the provided one. That is why it is important to use constants as keys. We also cache intercepted continuation in the `intercepted`
|
||||||
|
field. The field causes KNPE when we do not wrap the continuation with `SafeContinuation`.
|
||||||
|
|
||||||
|
#### Restricted Suspension
|
||||||
|
|
||||||
|
There are cases when we do not want to allow calling other suspend functions or lambdas from ours, for example, inside a lambda, passed
|
||||||
|
to `sequence` function, we want to call only `yield` and `yieldAll` functions, unless the functions we call inside the lambda, call `yield`
|
||||||
|
or `yieldAll`. Furthermore, we do not want to intercept their continuations. We want to limit them to the main thread. In this case, we use
|
||||||
|
`@RestrictsSuspension` annotation on classes or interfaces, which contain leaf suspend functions, which the lambda allowed to call.
|
||||||
|
If we look at `sequence`, the `SequenceScope` interface is annotated with the annotation.
|
||||||
|
|
||||||
|
Since we do not want to intercept the continuations, their `context`s cannot be other than `EmptyCoroutineContext`.
|
||||||
|
|||||||
Reference in New Issue
Block a user