Home Item 25 - Coroutines(English)
Post
Cancel

Item 25 - Coroutines(English)

What are Coroutines?

When many people see the new feature coroutines introduced in C++20, they might be puzzled and unsure how to use it.

Are coroutines the same as multithreading? The answer is: No!

Coroutines are not a multithreading technology but a method for performing non-blocking asynchronous operations within a single thread. Coroutines allow efficient task switching within a thread without the overhead of context switching and synchronization associated with multithreading. Understanding this point makes it much clearer when looking at the code.

Differences Between Coroutines and Multithreading

Coroutines:

  1. Cooperative multitasking within a single thread: Coroutines run within the same thread, achieving asynchronous operations by yielding and resuming control.
  2. Lightweight: Context switching in coroutines is much lighter because it doesn’t involve OS-level context switching.
  3. Yielding control: Coroutines can yield control when they need to wait, allowing other coroutines to run.
  4. Simplified asynchronous programming: Coroutines make asynchronous code look like synchronous code, simplifying the complexity of asynchronous programming.

Multithreading:

  1. Parallel execution on multiple threads: Multithreading allows code to run in parallel on multiple processor cores, suitable for CPU-intensive tasks.
  2. High context-switching overhead: Context switching in threads involves OS scheduling, which has higher overhead.
  3. Need for synchronization mechanisms: Threads need locks or other synchronization mechanisms to protect shared data and avoid race conditions.
  4. Complexity: Multithreading programming is usually more complex, prone to issues like deadlocks and race conditions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <coroutine>
#include <thread>

struct ReturnObject {
    struct promise_type {
        ReturnObject get_return_object() {
            return {
                .h = std::coroutine_handle<promise_type>::from_promise(*this)
            };
        }
        std::suspend_never initial_suspend() {
            return {};
        }
        std::suspend_never final_suspend() noexcept {
            return {};
        }
        void unhandled_exception() {
            std::terminate();
        }
        void return_void() {}
    };

    std::coroutine_handle<promise_type> h;
};

ReturnObject counter() {
    std::cout << "enter counter: " << std::endl;
    for (int i = 0; i < 3; ++i) {
        std::cout << "counter: " << i << std::endl;
        co_await std::suspend_always{};
        std::cout << "thread ID:[" << std::this_thread::get_id() << "]" << std::endl;
        std::cout << "counter after suspend_always: " << i << std::endl;
    }
    std::cout << "leave counter: " << std::endl;
}

int main() {

    std::cout << "main thread ID:[" << std::this_thread::get_id() <<"]" << std::endl;
    std::cout << "-----------" << std::endl;

    auto c = counter();
    for (int i = 0; i < 3; ++i) {
        std::cout << "main loop i: " << i << std::endl;
        c.h();
        std::cout << "-----------" << std::endl;
    }
    return 0;
}

Note

1
2
3
4
5
ReturnObject get_return_object() {
    return {
        .h = std::coroutine_handle<promise_type>::from_promise(*this)
    };
}

The .h file here uses designated initializer.

Execution Result:

Desktop View

  1. You can see that the main thread is executing from start to finish.
  2. The execution sequence is counter’s loop -> main’s loop -> counter’s loop…

Use Cases

Coroutines are suitable for the following scenarios:

  1. Asynchronous I/O operations: Such as network requests, file reading/writing, etc., where you need to wait for I/O operations to complete without blocking the main thread.
  2. Event-driven applications: Such as GUI applications, where you need to keep the interface responsive while handling user events.
  3. Game development: For handling asynchronous events like resource loading, network communication, etc.
  4. Cooperative multitasking: For efficiently switching tasks within a single thread, such as data stream processing, asynchronous computations, etc.

If you’re still feeling a bit fuzzy about coroutines, don’t worry. It’s common to feel this way initially. Just remember the most important point: Coroutines are not a multithreading technology.

The rest will become clear with more reading and practice.

☝ツ☝

This post is licensed under CC BY 4.0 by the author.

👈 ツ 👍