Improve article on coroutines
This commit is contained in:
parent
718e810c58
commit
3e754c21a2
1 changed files with 17 additions and 13 deletions
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
title: An Introduction to Coroutines
|
title: An Introduction to Coroutines
|
||||||
description: "A language-agnostic introduction to the concept of Coroutines"
|
description: "A language-agnostic introduction to the concept of Coroutines"
|
||||||
published: true
|
published: true
|
||||||
tags: beginner coroutines parallelism
|
tags: beginner coroutines parallelism
|
||||||
|
@ -21,7 +21,7 @@ In describing coroutines, the introductory sentence of the [wikipedia article](h
|
||||||
Put even more simply: A coroutine is *like a function* that can pause itself.
|
Put even more simply: A coroutine is *like a function* that can pause itself.
|
||||||
If we think of a normal function, the way it works is that we **call** the function, the function **executes** and at some point, it **returns** back to where it was called.
|
If we think of a normal function, the way it works is that we **call** the function, the function **executes** and at some point, it **returns** back to where it was called.
|
||||||
|
|
||||||
In a similar way, a very simple coroutine will do the same thing: We will **create** the coroutine, it will **execute** and at some point it will end and (implicitly) **yield** back to the calling code.
|
In a similar way, a very simple coroutine will do the same thing: We will **create** the coroutine, it will **execute** and at some point it will end and (implicitly) **yield** back to the calling code.
|
||||||
|
|
||||||
Here is a simple example of how this looks in practice.
|
Here is a simple example of how this looks in practice.
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ my_coro = coroutine(=> {
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
Now we can resume the coroutine up to 4 times, and each time it will resume from the last `yield` and stop at the next one, or when it reaches the end of the function.
|
Now we can resume the coroutine up to 4 times, and each time it will resume from the last `yield` and stop at the next one, or when it reaches the end of the subroutine.
|
||||||
|
|
||||||
```
|
```
|
||||||
first_yield = resume(coro)
|
first_yield = resume(coro)
|
||||||
|
@ -58,34 +58,38 @@ resume(coro) // prints "Done"
|
||||||
The big difference is: A coroutine can **yield** more than once, and will be paused in between. And that is really all there is to them, from a technical level. A simple example of this would look like this.
|
The big difference is: A coroutine can **yield** more than once, and will be paused in between. And that is really all there is to them, from a technical level. A simple example of this would look like this.
|
||||||
|
|
||||||
## Classifying Coroutines
|
## Classifying Coroutines
|
||||||
The 2004 paper [Revisiting Coroutines](http://www.inf.puc-rio.br/~roberto/docs/MCC15-04.pdf) classifies coroutines in two important ways: *Symmetric vs. Asymmetric* and *Stackful vs. Stackless*. The paper also distinguishes on whether coroutines are handled as values by the language, but that distinction is less important to understanding how they fundamentally work.
|
The 2004 paper [Revisiting Coroutines](http://www.inf.puc-rio.br/~roberto/docs/MCC15-04.pdf) classifies coroutines in two important ways: *Symmetric vs. Asymmetric* and *Stackful vs. Stackless*. The paper also distinguishes on whether coroutines are handled as values by the language, but that distinction is less important to understanding how they fundamentally work.
|
||||||
|
|
||||||
|
### Control Transfer Mechanism
|
||||||
|
|
||||||
### Control Transfer Mechanism
|
|
||||||
The way coroutines transfer control can happen in two ways.
|
The way coroutines transfer control can happen in two ways.
|
||||||
|
|
||||||
Asymmetric coroutines are more similar to how functions work. When a coroutine *A* resumes a coroutine *B*, *B* will at some point yield back to *A*, just like how any function will eventually return to its caller. We can think of these coroutines as organised in a stack, just like how functions are, but this is not the same as being *stackful*.
|
Asymmetric coroutines are more similar to how functions work. When a coroutine *A* resumes a coroutine *B*, *B* will at some point yield back to *A*, just like how any function will eventually return to its caller. We can think of these coroutines as organised in a stack, just like how functions are, but this is not the same as being *stackful*.
|
||||||
|
|
||||||
An important implication of this type of control transfer is that once a coroutine hands over control to another by resuming it, it can only ever be handed back control from this coroutine. In other words, it cannot be *resumed* from the outside, only *yielded* to.
|
An important implication of this type of control transfer is that once a coroutine hands over control to another by resuming it, it can only ever be handed back control from this coroutine. In other words, it cannot be *resumed* from the outside, as it is already "running", only *yielded* to.
|
||||||
|
|
||||||
To illustrate this: "When you lend a pencil to Bob, you know you will eventually get the pencil back from Bob and nobody else."
|
To illustrate this: "When you lend a pencil to Bob, you know you will eventually get the pencil back from Bob and nobody else."
|
||||||
|
|
||||||
This will be represented in pseudocode by the functions `resume` ("hand control down") and `yield` ("return control back up")
|
This will be represented in pseudocode by the functions `resume` ("hand control down") and `yield` ("return control back up")
|
||||||
|
|
||||||
|
In the previous section, the example represented this kind of coroutine. It handed over control three times using the `yield` function without specifying where to hand control back to. It simply went back to wherever the coroutine was resumed from.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Symmetric coroutines work a bit differently. Coroutines can freely transfer control to any other coroutine, instead of just "up and down".
|
Symmetric coroutines work a bit differently. Coroutines can freely transfer control to any other coroutine, instead of just "up and down".
|
||||||
|
|
||||||
Unlike asymmetric coroutines, this one-way control transfer means a coroutine can hand over control to another and be handed back control by a completely different one.
|
Unlike asymmetric coroutines, this one-way control transfer means a coroutine can hand over control to another and be handed back control by a completely different one.
|
||||||
|
|
||||||
Continuing the pencil analogy: "When you lend a pencil to Bob, you may later get it back from Steve, Larry, or never get it back at all."
|
Continuing the pencil analogy: "When you lend a pencil to Bob, you may later get it back from Steve, Larry, or never get it back at all."
|
||||||
|
|
||||||
In pseudocode, this will be represented by the `transfer` function.
|
In pseudocode, this will be represented by the `transfer` function.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
The main advantage of asymmetric coroutines is that they offer more structure. Symmetric coroutines let the user freely jump back and forth between coroutines, in a similar way to `goto` statements, which can make core hard to follow.
|
The main advantage of asymmetric coroutines is that they offer more structure. Symmetric coroutines let the user freely jump back and forth between coroutines, in a similar way to `goto` statements, which grants a much higher degree of freedom but can also make code hard to follow.
|
||||||
|
|
||||||
### Stackfulness
|
### Stackfulness
|
||||||
|
|
||||||
Another important way to categorize coroutines is whether every coroutine has its own stack. This distinction is much harder to explain in theory, but will become clear in examples later on.
|
Another important way to categorize coroutines is whether every coroutine has its own stack. This distinction is much harder to explain in theory, but will become clear in examples later on.
|
||||||
|
|
||||||
Stackless coroutines, as the name implies, don't have their own call stack. What this means in practice is that the program has no way of tracking their call stack once they yield control, so this is only possible from the function on the bottom of the stack.
|
Stackless coroutines, as the name implies, don't have their own call stack. What this means in practice is that the program has no way of tracking their call stack once they yield control, so this is only possible from the function on the bottom of the stack.
|
||||||
|
@ -98,9 +102,9 @@ A complete explanation of why this is and how it works could easily be its own a
|
||||||
|
|
||||||
Many programming languages actually have *stackless* *asymmetric* coroutines; JavaScript, for example, calls them *generators*.
|
Many programming languages actually have *stackless* *asymmetric* coroutines; JavaScript, for example, calls them *generators*.
|
||||||
|
|
||||||
Some languages even have a mix of both: Ruby *fibers* can both use `resume`/`yield` semantics, but they can also freely transfer control freely with the `transfer` method.
|
Some languages even have a mix of both: Ruby *fibers* can both use `resume`/`yield` semantics, but they can also freely transfer control freely with the `transfer` method.
|
||||||
|
|
||||||
Windows even provides an OS-level API for coroutines: It calls them Fibers, and they are *stackful* and *symmetric*. Linux does not provide any coroutine API yet.
|
Windows even provides an OS-level API for coroutines: It calls them Fibers, and they are *stackful* and *symmetric*. Linux does not provide any coroutine API yet.
|
||||||
|
|
||||||
## Why are they useful?
|
## Why are they useful?
|
||||||
On an abstract level, the strength of coroutines is to manage state. Since they remember where they left off for the next time they're resumed, they can use control-flow to save state that would otherwise have to be stored in variables.
|
On an abstract level, the strength of coroutines is to manage state. Since they remember where they left off for the next time they're resumed, they can use control-flow to save state that would otherwise have to be stored in variables.
|
||||||
|
@ -165,7 +169,7 @@ object.move_right = distance ⇒ {
|
||||||
|
|
||||||
This could be refactored into a single `move(axis, distance)` function, of course; but the additional code to figure out the direction would clutter the code a bit too much for this example.
|
This could be refactored into a single `move(axis, distance)` function, of course; but the additional code to figure out the direction would clutter the code a bit too much for this example.
|
||||||
|
|
||||||
The important thing here is, that the top-level function of the coroutine never `yield`s; instead, it calls a `move_*` function that takes care of yielding itself. This is where **stackfulnes** comes into play again: Only **stackful** coroutines can do this. In languages with stackless coroutines, like javascript, code like this would likely be rejected by the compiler.
|
The important thing here is, that the top-level function of the coroutine never `yield`s; instead, it calls a `move_*` function that takes care of yielding itself. This is where **stackfulnes** comes into play again: Only **stackful** coroutines can do this. In languages with stackless coroutines, like javascript, code like this would likely be rejected by the compiler.
|
||||||
|
|
||||||
Put very simply: the reason for this is that when `move_right` yields, it needs to remember where it needs to return to after it resumes. This information is what's on the stack, so a coroutine without its own stack cannot remember from nested functions.
|
Put very simply: the reason for this is that when `move_right` yields, it needs to remember where it needs to return to after it resumes. This information is what's on the stack, so a coroutine without its own stack cannot remember from nested functions.
|
||||||
|
|
||||||
|
@ -174,8 +178,8 @@ Another application of coroutines is handling the complexity if asynchronous cod
|
||||||
|
|
||||||
But how exactly can coroutines help with this? Simple: by yielding to an event-loop, which will resume them once a certain event happens. While in practice this is a bit more complicated, the core idea is this:
|
But how exactly can coroutines help with this? Simple: by yielding to an event-loop, which will resume them once a certain event happens. While in practice this is a bit more complicated, the core idea is this:
|
||||||
|
|
||||||
* All code runs inside coroutines (In some languages this is always the case, in others the framework would have to wrap the user code manually)
|
* All code runs inside coroutines (In some languages this is always the case, in others it would be up to a framework to run any user code in a coroutine)
|
||||||
* Functions that need to await asynchronous tasks yield from the current coroutine
|
* Functions that need to await asynchronous tasks simply start an I/O operation, then yield some object describing when to resume them back to the top level coroutine
|
||||||
* When a coroutine yields, a scheduler will decide what coroutine to resume next, or simply sleep until any new "event" is available.
|
* When a coroutine yields, a scheduler will decide what coroutine to resume next, or simply sleep until any new "event" is available.
|
||||||
|
|
||||||
This should sound very familiar to anybody who has worked with `async`/`await` before. It is a very similar concept. Then only difference is that functions are always synchronous, and all functions are implicitly awaited.
|
This should sound very familiar to anybody who has worked with `async`/`await` before. It is a very similar concept. Then only difference is that functions are always synchronous, and all functions are implicitly awaited.
|
||||||
|
|
Loading…
Reference in a new issue