To start, if a function is a routine or a subroutine, then an async function is a coroutine.
In the main program, there is a main stack that contains the memory space allocated to execute a function. This space is called a stack frame, and it stores local variables and parameters. The stack also contains the variables and stack frames of the functions being executed.
When you return from a function, you remove the stack frame from the main stack, and you lose the local variables of that function.
In a coroutine, you can pause the execution and return control to where you were before without
removing anything from the stack. This is an async
function.
Let's consider this code snippet, which includes a main
function and a foo
coroutine that you can pause.
1 main () {
2 foo()
3 }
4 foo() {
5 suspend
6 }
We can use the async
keyword to indicate that foo
will give us a pointer to
the stack frame of the coroutine. This allows us to return to where we were in the main function.
Keep in mind that this behavior is only a convention, and we could return additional data about the
coroutine if we wanted to. Similarly, we can use the resume
keyword to indicate that we
want to return to the stack frame. It's worth noting that this is a syntax convention that I've
created.
1 main () {
2 f = async foo()
3 resume f()
4 }
5 foo() {
6 suspend
7 return 10
8 }
- Begin by loading the main function at LOC 1.
- The stack contains the execution line and the variables of the main function and its frames.
- At LOC 2, call the
foo()
function and save the pointer to the frame. - At LOC 6, you are inside the stack frame of
foo()
, where you make asuspend
call and return execution to the caller without popping the frame. - At LOC 3, you
resume
the frame. - At LOC 7, you
return
the value 10 and pop the frame. - Now you are at LOC 4. If you had saved the value in a variable, you could use it normally. After all, it's just another variable in the stack, right?
How does the following example work? What does it print?
1 main () {
2 f = async foo()
3 v = resume f()
4 print(v)
5 }
6 foo() {
7 suspend
8 return 10
9 }
And what about this one? What does it print?
1 var global = 0
2 main () {
3 f = foo()
4 while (global != 3) {
5 resume f
6 }
7 print(global)
8 }
9 foo() {
10 while (1) {
10 global++
11 suspend
12 }
13}
The await
keyword causes the program to wait in the main function instead of continuing
with the execution. There's nothing mysterious about it; it simply waits for the frame to return.
This makes it ideal for IO tasks, where you may need to wait for a response. For example, you might
do some work in the background and then await the response.
Function coloring
The reason that async
functions need to go with async is that when managing memory,
there can be situations where you have uninitialized variables. If you execute code before the
await
and after the async
function, it could print garbage because there
are uninitialized pointers waiting for a value. Sometimes the compiler can detect this, but other
times it can't.
It's worth noting that we're just using pointers to execute the functions and coroutines. This means that you can run them in any thread or scheduler you need, depending on your requirements.
In conclusion, it's important to understand that coroutines have nothing to do with parallelism. They are simply a way to manage memory and handle IO tasks in an efficient and elegant way.
Blocking, non-blocking
In a subroutine, the caller always waits for the return value. However, with coroutines and
async
functions, the caller only waits for the result if they use the
await
keyword. The use of suspend
in the function body doesn't determine
whether the caller has to block or not.
The decision to wait or not is up to the caller, and whether a function is
async
or not doesn't determine whether it blocks or is non-blocking. Instead, it's the
use of await
that matters in determining whether a caller needs to block or not.
More
This post was an introduction with some inaccuracies. Read this for more.