Document coroutines codegen: state-machine
This commit is contained in:
committed by
Ilmir Usmanov
parent
5fd08b3ee5
commit
1d7a661624
@@ -179,3 +179,77 @@ Exception in thread "main" java.lang.IllegalStateException: Already resumed
|
|||||||
The last line is what we get when we try to resume a finished continuation.
|
The last line is what we get when we try to resume a finished continuation.
|
||||||
|
|
||||||
In this little example happens a lot. The rest of the section explains it bit by bit, starting with a state-machine.
|
In this little example happens a lot. The rest of the section explains it bit by bit, starting with a state-machine.
|
||||||
|
|
||||||
|
### State-Machine
|
||||||
|
|
||||||
|
The compiler turns sequential code into suspendable by using state machines. It distributes suspending calls between states in a
|
||||||
|
state-machine. The relationship between the calls and the states is one-to-one: each call gets a state, and each state gets a call. The
|
||||||
|
state ends with the call, and the compiler places all instructions preceding the call in the same state before the call. It places all
|
||||||
|
instructions after the last call in a separate state.
|
||||||
|
|
||||||
|
For example, having
|
||||||
|
```kotlin
|
||||||
|
dummy()
|
||||||
|
println(1)
|
||||||
|
dummy()
|
||||||
|
println(2)
|
||||||
|
```
|
||||||
|
the compiler splits the code in the following way
|
||||||
|
```text
|
||||||
|
==========
|
||||||
|
dummy()
|
||||||
|
----------
|
||||||
|
println(1)
|
||||||
|
dummy()
|
||||||
|
----------
|
||||||
|
println(2)
|
||||||
|
==========
|
||||||
|
```
|
||||||
|
where function boundaries are represented by `==========` and state boundaries are represented by `----------`. The compiler after splitting
|
||||||
|
the function generates the following code (simplified for now):
|
||||||
|
```kotlin
|
||||||
|
val $result: Any? = null
|
||||||
|
when(this.label) {
|
||||||
|
0 -> {
|
||||||
|
this.label = 1
|
||||||
|
$result = dummy(this)
|
||||||
|
if ($result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
|
||||||
|
goto 1
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
println(1)
|
||||||
|
this.label = 2
|
||||||
|
$result = dummy(this)
|
||||||
|
if ($result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
|
||||||
|
goto 2
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
println(2)
|
||||||
|
return Unit
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw IllegalStateException("call to 'resume' before 'invoke' with coroutine")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Then it puts the state-machine inside the `invokeSuspend` function. Thus, in addition to the usual for lambda captured parameters, `<init>`
|
||||||
|
and `invoke`, we have `label` field and `invokeSuspend` function.
|
||||||
|
|
||||||
|
At the beginning of the function `label`'s value is `0`. Before the call, we set it to `1`. During the call, two things can happen:
|
||||||
|
1. `dummy` returns a result, in this case, `Unit`. When this happens, execution continues as if it was sequential code, jumping to the next
|
||||||
|
state.
|
||||||
|
2. `dummy` suspends. When a suspending function suspends, it returns the `COROUTINE_SUSPENDED` marker. So, if `dummy` suspends,
|
||||||
|
the caller also suspends by returning the same `COROUTINE_SUSPENDED`. Furthermore, all the suspend functions in call stack suspend,
|
||||||
|
returning COROUTINE_SUSPEND, until we reach the coroutine builder function, which just returns.
|
||||||
|
|
||||||
|
Upon resume, `invokeSuspend` is called again, but this time `label` is `1`, so the execution jumps directly to the second state, and the
|
||||||
|
caller does not execute the first call to `dummy` again. This way, the lambda's execution can be suspended and resumed. Thus the lambda
|
||||||
|
is turned into a coroutine, which is, by definition, is a suspendable unit of code.
|
||||||
|
|
||||||
|
That is the reason why we need to turn linear code into a state machine.
|
||||||
|
|
||||||
|
On a closing note, the state machine should be flat; in other words, there should be no state-machine inside the state of a state-machine.
|
||||||
|
Otherwise, inner state-machine states will rewrite `label`, breaking the whole suspend-resume machinery and leading to weird behavior,
|
||||||
|
ranging from CCE to infinite loops. Similar buggy behavior happens when several suspending calls are in one state: when the first call
|
||||||
|
suspends, and then the execution resumes, skipping all the remaining code in the state. Both these bugs were quite frequent in the early
|
||||||
|
days of coroutines inlining.
|
||||||
Reference in New Issue
Block a user