https://lilymonade.fr/blog/feed.xml

Having breakfast with coroutines

2026-04-19

Here is yet another attempt on the internet to explain concurrency and parallelism, their difference, and why we use them both all the time. This idea appeared to me when listening to a CppCon 2026 talk while cooking. What I was doing was exactly what was explained.

And I started thinking more and more about it and cooking really seems to be the best analogy in explaining those concepts, because the kitchen is in the end a full system with CPU (you), peripherals (Kettle, Oven, microwave...), data (ingredients), programs to run (recipes)...

My usual breakfast

I like to start my day with some cafein, but not too much. So I brew some tea. And since I'm French, I love bread, and so I make some jam toasts 🤤 (I'm trying to be vegan, so no butter 🫡). And those two things are independent coroutines. Don't trust me ? Let me explain.

Task 1: Brewing tea

This task is really simple and does not involve a lot of work. It is almost 100% waiting.

  • Fill the kettle
  • Start the kettle
  • Once the water is hot, pour it in a mug
  • Put some tea in the mug
  • Wait for the tea to infuse, and the water to cool down

In Rust, using an async/await style, you could represent this coroutine like this:

async fn brew_tea() {
    fill_kettle();
    make_water_boil().await;
    put_tea();
    infuse_tea().await;
}

fill_kettle() and put_tea() are rather simple and fast tasks that require us (the CPU) to move things (data) around, so of course it cannot be asynchronous tasks, but it's ok since most of the time is spent in make_water_boil(), and infuse_tea(). Those tasks don't require us, they are things that we need to start, but only wait for them to finish. This is why we can represent them as async tasks, and so we must await them.

Task 2: Making jam toats

Making toasts, on the contrary, is a rather busy task. You don't wait, always work.

  • Cut the baguette into N toasts
  • For each toast
    • Spread the jam on it

Again, in Rust, this coroutine could be written as:

async fn make_jam_toasts() {
    let toasts = cut_baguette();
    for toast in toasts {
        spread_jam(toast);
    }
}

Note here that, for the moment, we can totaly make this function 100% sequential and remove the async. But it will change.

What is concurrency

So now you decide to run this little program consisting of 2 concurrent (async) tasks. You start task 1 by filling the kettle, then you start the heating element, and ... Nothing ? You wait, staring at the water, thinking about your shitty job and your last landlord stupid request ?.. But I digress...

No of course you have free time you can use it to work on the second task. This is concurrency: you stoped working on an unfinished task to work on another one. You have now 2 tasks started at the same time but since you are alone you can only work on one at any given time. This is also called multitasking, litteraly meaning "doing multiple tasks", in the sens of doing a little bit of each at a time.

So you start and cut some bread, and spread jam on the toasts... About halfway through task 2 you hear the water is boiling. You could stop task 2 and resume task 1, but for now make_jam_toasts() does not have any await points. If we followed this program, we would need to fully finish make_jam_toasts() before resuming brew_tea().

But task 2 takes time, and the water is cooling down ... So you decide to pause task 2 after finishing one toast to pour the water in the mug. We can also do this in Rust using simple async tasks often called yield_now in most async runtimes:

async fn make_jam_toasts() {
    let toasts = cut_baguette();
    for toast in toasts {
        spread_jam(toast);

        // after we do 1 toast, we pause and check if any other task need our attention
        yield_now().await;
    }
}

Now that we have an await point, the task takes pauses some times. So we stoped working on task 2, resuming task 1 to quickly pour the water, put the tea, and paused task 1 again to let the water cool down and the tea infuse. You continue making toasts, checking if the tea is ready after each one, and once the tea and your toasts are ready you can enjoy your breakfast.

What is parallelism

Parallelism is when two things are advancing at the same time. It seems we don't have parallelism in this example. Of course we don't have human parallelism, since we don't have 2 humans to execute multiple human tasks at the same time. But we still have 2 things working in parallel.

You, making toasts, and the kettle, boiling water. Yes, the kettle, like all peripherals of your computer, can work on its own. And you can both work in parallel. Of course, you must wait for the kettle to continue working on task 1, but you can making progress for task 2.

Think about what happens when you write a file in your disk. Once a job is sent to your storage device, it starts copying data from RAM to itself (writing) or copying data from itself to the RAM (reading). This process take a lot of time, and the CPU would waste it time waiting for the job to finish, so we designed OSs so that other tasks still advance until the task that was waiting for a disk operation is ready to continue.

The two types of concurrency

In this story we saw a type of concurrency called cooperative concurrency. Each task/coroutine conciously make pauses at some points. In Rust, a coroutine can pause each time you see a .await. This is a bit more complex than that but you get the idea. If you don't see any .await, the task will continue and no one will force it to pause.

This can be a problem, imagine if we did not put yield points in our second task then by the time we finished making toasts the water would be cold... But for some reasons, sometimes, it is very hard to know where to pause, because pausing has a cost and if you spend most of your time switching from task to task just to check if something is ready you may as well simply do your tasks sequentialy and wait for the damn water to boil without touching anything else (this can happen if you are tired or depressed, I know 🥲, it's ok it gets better trust me).

So it would be nice if we get notified when other concurrent tasks are ready to advance, and if something else tells us to pause what we are doing right now and do another task. This mechanism exists, and is called preemption, or preemptive concurrency.

Preemption is what is used by your operating system to work on many many many tasks at the same time. Instead of relying on tasks to gently pause from time to time, it uses a CPU's mecanism called interrupts to forcibly switch tasks when some event occurs. This event can be many things, and it is often related to a clock ticking at a given rate, or a peripheral notifying its job is done. In our kitchen scenario it can be "the water is hot", or "your tea has infused".