preface
Many languages implement coroutines, such as
- Generators in Python
- In Lua Coroutine
- Go in the routine
- Js the async/await
This article focuses on the basics of coroutines, as well as the use of Kotlin handwritten python generator, including the following contents 1. What is a coroutine? 2. What does a coroutine do? 3. 5. How do coroutines work in Python? 6
The basics of coroutines
What is a coroutine
- A coroutine is a program that can be suspended and resumed by the program itself
- Coroutines can be used for collaborative execution of multiple tasks
- Coroutines can be used to solve the asynchronous flexible transfer in the task control flow
What does a coroutine do
- Coroutines can synchronize asynchronous code
- Coroutines can reduce the complexity of asynchronous code
- Synchronous code is more flexible than asynchronous code, making it easier to implement complex business
What is the difference between coroutines and threads?
Threads are preemptive and coroutines are collaborative
Threads rotate according to time slices and are scheduled by the operating system
Coroutines are programs that cooperate with each other
As shown in the figure below:
Classification of coroutines
According to the call stack
1. Stack coroutines: Each coroutine will be allocated a separate call stack, similar to the thread call stack 2. Stackless coroutines: no separate call stack is allocated, and the hanging start state is saved by closures or objects
By call relationship
1. Symmetric coroutine: the scheduling power can be transferred to any coroutine, and the relationship between coroutines is equal 2. Asymmetric coroutines: scheduling rights can only be transferred to the coroutines that call themselves. Coroutines have a parent-child relationship
Python and Kotlin coroutines are stackless asymmetric coroutines
What do Python coroutines look like?
Coroutines are also found in Python syntax. To understand a coroutine, you need to know what a generator is. Generators are functions that produce values, but they use the keyword yield to produce values. Here’s an example:
def gen() :
n = 0
while True:
yield n
n += 1
g = gen()
print(g) # <generator object gen at 0x00000246E165A7C8>
print(next(g)) # Output 0
print(next(g)) # output 1
Copy the code
When we call the gen() function, we don’t execute the function directly. Instead, we get a generator object. Calling next() on the generator object will start execution at the first yield, yielding a value of 0. Note that gen() pauses at yield until next() is called a second time.
As we can see here, a generator function is a pausable function, which is driven by the caller (who uses the next() function) to pause its execution every time it reaches yield and outputs the value after yield to the caller until the caller drives it again.
Kotlin implements Python coroutines
Now how can we do the same thing with Kotlin
1. Take a look at the final calling code to implement
fun main(a) {
val nums = generator { start: Int ->
// Limit the range of yield calls to be used only in lambda
for (i in 0.. 5) {
yield(start + i)
}
}
val seqs = nums(10)
for (j in seqs){
println(j)
}
}
Copy the code
The above is what we want to achieve in the end. As you can see, we need to implement the following: 1. the generator method 2. the results obtained by the generator method need to support the Iterator interface 3.yield methods 4.yield methods need to be limited
2.Generator interface and implementation
Defining the Generator interface
interface Generator<T> {
operator fun iterator(a): Iterator<T>
}
Copy the code
Defining the Generator implementation
class GeneratorImpl<T>(
private val block: suspend GeneratorScope<T>.(T) -> Unit.private val parameter: T
) : Generator<T> {
override fun iterator(a): Iterator<T> {
return GeneratorIterator(block, parameter)
}
}
Copy the code
Define GeneratorScope
To limit the use of yield methods, you need to define GeneratorScope
abstract class GeneratorScope<T> internal constructor() {
protected abstract val parameter: T
abstract suspend fun yield(value: T)
}
Copy the code
Write the Generator method
With this defined, we can write the Generator method
fun <T> generator(block: suspend GeneratorScope<T>. (T) - >Unit): (T) -> Generator<T> {
return {
GeneratorImpl(block, it)
}
}
Copy the code
When lambda is used as a parameter function declaration, it can carry a receiver, as shown in the figure below:A lambda with a receiver enriches the function declaration, and when the lambda value is passed, the receiver is carried, such as:
// Declare the receiver
fun kotlinDSL(block:StringBuilder. () - >Unit){
block(StringBuilder("Kotlin"))}// Call higher-order functions
kotlinDSL {
// The receiver type of this lambda is StringBuilder
append(" DSL")
println(this} >>> Output Kotlin DSLCopy the code
3. The implementation of GeneratorIterator
Defining generator state
First, we need to define the state of the generator to facilitate the subsequent judgment of whether the generator is ready or not, and make the state transition according to the corresponding conditions
sealed class State {
class NotReady(val continuation: Continuation<Unit>) : State()
class Ready<T>(val continuation: Continuation<Unit>, val nextValue: T) : State()
object Done : State()
}
Copy the code
As shown above, three states are defined: 1.NotReady, NotReady, initial, pending 2.Ready, Ready, Ready to return 3
1. The sealed class is a class that restricts the class hierarchy. 2. You can declare a class as sealed by using the sealed keyword before the class name. 3. It is used to represent a restricted class hierarchy. 4. Use a sealed class when an object has one of the types from a finite set but cannot have any other types. 5. The constructor of a sealed class is private by default, and it cannot be declared non-private.
The main advantage of sealing classes when used with when is that a subclass of sealing classes treats its own type as a case. Therefore, the WHEN expression in the sealed class covers all cases, thereby avoiding the else clause.
The specific implementation
class GeneratorIterator<T>(
private val block: suspend GeneratorScope<T>.(T) -> Unit.override valparameter: T ) : Iterator<T>, Continuation<Any? >, GeneratorScope<T>() {override val context: CoroutineContext = EmptyCoroutineContext
private var state: State
init {
val coroutineBlock: suspend GeneratorScope<T>.() -> Unit = {
block(parameter)
}
val start = coroutineBlock.createCoroutine(this.this)
state = State.NotReady(start)
}
override fun hasNext(a): Boolean {
resume()
returnstate ! = State.Done }override fun next(a): T {
return when (val currentState = state) {
is State.NotReady -> {
return next()
}
is State.Ready<*> -> {
state = State.NotReady(currentState.continuation)
(currentState as State.Ready<T>).nextValue
}
State.Done -> throw IndexOutOfBoundsException("No Value Left")}}override fun resumeWith(result: Result<Any? >) {
state = State.Done
result.getOrThrow()
}
override suspend fun yield(value: T) {
return suspendCoroutine {
state = when (state) {
is State.NotReady -> State.Ready(it, value)
is State.Ready<*> -> throw IllegalStateException("Cannot yield a value while ready")
State.Done -> throw IllegalStateException("Cannot yield a value while Done")}}}private fun resume(a) {
when (val currentState = state) {
is State.NotReady -> currentState.continuation.resume(Unit)}}}Copy the code
The main traversal, suspend, and restore logic is then defined in generatorIterator1. Call coroutineBlock. CreateCoroutine create coroutines 2. Call continuation. Resume starts 3. Call the passed block, i.e. yield, suspend the function, and update status 4. Return the value in the next method and update the status 5. Then resume the coroutine in the hasNext method and iterate to the next 6. Finally, resumeWith is called when finished, with the state set to Done
The above source code can be seen: Kotlin handwritten Python coroutine source code
The resources
A Python coroutine with a receiver’s lambda expression, Kotlin Sealed class