Document coroutines codegen: intrinsics
This commit is contained in:
committed by
Ilmir Usmanov
parent
0943a31675
commit
611c1b1a38
@@ -794,3 +794,148 @@ INVOKESTATIC InlineMarker.mark
|
|||||||
ARETURN
|
ARETURN
|
||||||
```
|
```
|
||||||
Then we will have the same three locals to spills, instead of four.
|
Then we will have the same three locals to spills, instead of four.
|
||||||
|
|
||||||
|
### Coroutine Intrinsics
|
||||||
|
|
||||||
|
Previous examples had an elementary coroutine builder. They used so-called empty continuation. Let us now recreate kotlinx.coroutines'
|
||||||
|
`async` function, which runs a coroutine on another thread and then, upon its completion, returns the result to the main thread.
|
||||||
|
|
||||||
|
First, we need a class which waits on the main thread:
|
||||||
|
```kotlin
|
||||||
|
class Async<T> {
|
||||||
|
suspend fun await(): T = TODO()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
then the root continuation:
|
||||||
|
```kotlin
|
||||||
|
class AsyncContinuation<T>: Continuation<T> {
|
||||||
|
override val context = EmptyCoroutineContext
|
||||||
|
|
||||||
|
override fun resumeWith(result: Result<T>) {
|
||||||
|
TODO()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Now, we can piece these two classes together. In `await`, we should check whether a coroutine already computed the result and if so, return
|
||||||
|
it by using `it.resume(value)` trick from resume with result section. Otherwise, we should save the continuation, so it can be resumed when
|
||||||
|
the result is available. Inside the continuation's `resumeWith`, we should check whether we await the result and resume the awaiting
|
||||||
|
continuation with the computed result; otherwise, we save the result so that it will be accessible in `await`. In code, it will look like:
|
||||||
|
```kotlin
|
||||||
|
class AsyncContinuation<T>: Continuation<T> {
|
||||||
|
var result: T? = null
|
||||||
|
var awaiting: Continuation<T>? = null
|
||||||
|
override val context: CoroutineContext
|
||||||
|
get() = EmptyCoroutineContext
|
||||||
|
|
||||||
|
override fun resumeWith(result: Result<T>) {
|
||||||
|
if (awaiting != null) {
|
||||||
|
awaiting?.resumeWith(result)
|
||||||
|
} else {
|
||||||
|
this.result = result.getOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Async<T>(val ac: AsyncContinuation<T>) {
|
||||||
|
suspend fun await(): T =
|
||||||
|
suspendCoroutine<T> {
|
||||||
|
val res = ac.result
|
||||||
|
if (res != null)
|
||||||
|
it.resume(res)
|
||||||
|
else
|
||||||
|
ac.awaiting = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, we can write the builder itself:
|
||||||
|
```kotlin
|
||||||
|
fun <T> async(c: suspend () -> T): Async<T> {
|
||||||
|
val ac = AsyncContinuation<T>()
|
||||||
|
c.startCoroutine(ac)
|
||||||
|
return Async(ac)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
and simple main function to test, that everything works as expected (again, check with suspension just to be sure we did not miss
|
||||||
|
anything):
|
||||||
|
```kotlin
|
||||||
|
fun main() {
|
||||||
|
var c: Continuation<String>? = null
|
||||||
|
builder {
|
||||||
|
val async = async {
|
||||||
|
println("Async in thread ${Thread.currentThread().id}")
|
||||||
|
suspendCoroutine<String> { c = it }
|
||||||
|
}
|
||||||
|
println("Await in thread ${Thread.currentThread().id}")
|
||||||
|
println(async.await())
|
||||||
|
}
|
||||||
|
c?.resume("OK")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Upon running, it will print
|
||||||
|
```text
|
||||||
|
Async in thread 1
|
||||||
|
Await in thread 1
|
||||||
|
OK
|
||||||
|
```
|
||||||
|
Since it is not multithreaded yet, it will run `async`'s coroutine in the main thread. However, before we make it multithreaded, we need to
|
||||||
|
cover how the `suspendCoroutine` function works.
|
||||||
|
|
||||||
|
#### suspendCoroutine
|
||||||
|
|
||||||
|
After explaining in depth how resume works, which hoops the compilers jumps through to generate a (correct) state machine, let's see, what
|
||||||
|
happens, when we call `suspendCoroutine`. We now know two pieces about the function: it somehow returns `COROUTINE_SUSPENDED` and it
|
||||||
|
provides access to continuation parameter. The function is defined as follows:
|
||||||
|
```kotlin
|
||||||
|
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T =
|
||||||
|
suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
|
||||||
|
val safe = SafeContinuation(c.intercepted())
|
||||||
|
block(safe)
|
||||||
|
safe.getOrThrow()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
So, it does five different things:
|
||||||
|
1. accesses continuation argument
|
||||||
|
2. intercepts continuation
|
||||||
|
3. wraps it in `SafeContinuation`
|
||||||
|
4. calls the lambda argument
|
||||||
|
5. returns result, `COROUTINE_SUSPENDED` or throws an exception
|
||||||
|
|
||||||
|
#### suspendCoroutineUninterceptedOrReturn
|
||||||
|
|
||||||
|
First, let us examine how one can access a continuation argument without suspending current executions.
|
||||||
|
`suspendCoroutineUninterceptedOrReturn` is an intrinsic function that does only one thing: inlines provided lambda parameter passing
|
||||||
|
continuation parameter to it. Since its purpose is to give access to the continuation argument, which is invisible in suspend functions
|
||||||
|
and lambdas. Thus we cannot write in pure Kotlin. It has to be intrinsic.
|
||||||
|
|
||||||
|
Fun fact: since the lambda returns `returnType | COROUTINE_SUSPENDED`, the compiler does not check its return type, so there can be some
|
||||||
|
funny CCEs at runtime because of this unsoundness in the Koltin type system:
|
||||||
|
```kotlin
|
||||||
|
import kotlin.coroutines.intrinsics.*
|
||||||
|
|
||||||
|
suspend fun returnsInt(): Int = suspendCoroutineUninterceptedOrReturn { "Nope, it returns String" }
|
||||||
|
|
||||||
|
suspend fun main() {
|
||||||
|
1 + returnsInt()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
will throw
|
||||||
|
```text
|
||||||
|
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
|
||||||
|
```
|
||||||
|
|
||||||
|
Furthermore, the runtime throws the CCE upon the usage of the return value. So, if one just ignores its return value or even better (I mean,
|
||||||
|
worse) calls the function in tail-call position (to enable tail-call optimization), no exception is thrown. So, the next example
|
||||||
|
runs just fine:
|
||||||
|
```kotlin
|
||||||
|
import kotlin.coroutines.intrinsics.*
|
||||||
|
|
||||||
|
suspend fun returnsInt(): Int = suspendCoroutineUninterceptedOrReturn { "Nope, it returns String" }
|
||||||
|
|
||||||
|
suspend fun alsoReturnsInt(): Int = returnsInt()
|
||||||
|
|
||||||
|
suspend fun main() {
|
||||||
|
returnsInt()
|
||||||
|
alsoReturnsInt()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user