Overview of Coroutines in Linux

Overview of Coroutines in Linux

The premise of this article: This is a non-technical article that discusses what coroutines are and the current usage of coroutines, rather than insisting on their necessity.

In Linux, coroutine switching is a type of lightweight context switching, which is more efficient than thread switching. Coroutines are a lighter execution unit than threads, typically managed by user-space libraries rather than the operating system kernel. Therefore, coroutine switching does not require intervention from the operating system kernel; they rely solely on user-space management. To understand coroutine switching, it is essential to first grasp the concepts of coroutines and context switching.

1. What are Coroutines?

Coroutines are a type of user-level thread that executes within a single thread. Unlike threads, coroutine scheduling does not depend on the operating system’s scheduler but is managed by the application or coroutine libraries (such as <span>libco</span>, <span>libuv</span>, <span>libtask</span>, <span>Go</span>‘s goroutines, etc.) in user space. Coroutines are very lightweight because they share the address space of a thread and only perform context switching when necessary.

Characteristics of coroutines:

  • Coroutines control when to switch through cooperative scheduling, where coroutines voluntarily yield control, unlike threads that are forcibly switched by the operating system scheduler.

  • Switching between coroutines does not require the involvement of the operating system kernel, typically only needing to save and restore a small number of registers and stack information (compared to the complete context switching of threads, coroutine switching is very lightweight).

  • Since coroutine scheduling is managed in user space, it does not involve system calls, resulting in lower performance overhead during switching.

2. Context Switching of Coroutines

The switching of coroutines typically involves the following steps:

  1. Saving the current coroutine’s context: This involves saving the current coroutine’s execution state, such as the stack pointer, program counter (PC), registers, etc. This information is saved as the coroutine’s context for later restoration.

  2. Switching to the target coroutine: Restoring the target coroutine’s context, including stack, registers, etc., and continuing execution from where it last stopped.

  3. Executing the target coroutine’s task: Once the switch is complete, the target coroutine begins execution until it voluntarily yields control or completes its task.

Context Content of Coroutine Switching:

  • Stack Pointer: Coroutines have independent stack space, and the stack pointer points to the top of the current coroutine’s stack.

  • Program Counter (PC): Records where the current execution is, needing to be saved and restored during switching.

  • Registers: Saves the current coroutine’s register state, especially the CPU’s working registers, which need to be saved and restored during switching.

  • Coroutine’s Local Variables and Heap Memory: Coroutine’s local variables are stored on the stack, while heap memory is managed by the operating system, but it is not lost during switching, as coroutines share heap memory.

3. Implementation Mechanisms of Coroutine Switching

In Linux, coroutine switching is typically managed by user-level thread libraries, with the most common implementations including manual context switching or using setjmp/longjmp or asm techniques to save and restore context.

1. Manual Context Switching:<span>setjmp</span>/<span>longjmp</span>

<span>setjmp</span> and <span>longjmp</span> are a pair of functions in the C standard library that can be used to save and restore the execution state of a program. These functions are commonly used to implement context switching for coroutines or user-level threads.

  • <span>setjmp</span> is used to save the current execution context (such as program counter, stack pointer, etc.).

  • <span>longjmp</span> is used to restore the context previously saved by <span>setjmp</span>, jumping back to the saved location to continue execution.

Using <span>setjmp</span> and <span>longjmp</span> can conveniently implement coroutine switching, but this method requires manual management of the stack and context, which can be complex.

2. Coroutine Switching Based on <span>ucontext.h</span> Implementation

In Linux, <span>ucontext.h</span> provides functions that allow programmers to manage thread contexts, thus enabling coroutine switching. For example, <span>getcontext</span> and <span>setcontext</span> functions can be used to get and set execution contexts.

  • <span>getcontext</span>: Gets the current thread’s context.

  • <span>setcontext</span>: Restores and executes the specified context.

  • <span>makecontext</span>: Sets a context to execute a specified function.

This method can also be used to implement coroutine scheduling and switching, but <span>ucontext.h</span> is no longer recommended for use in modern Linux systems due to some limitations, and certain features may not be fully supported in newer systems.

3. Assembly-Based Implementation

Some efficient coroutine libraries use assembly language to implement coroutine switching. By using assembly language, we can directly manipulate CPU registers to save and restore coroutine contexts. This method can achieve more efficient coroutine switching but typically requires more manual work and knowledge of assembly.

For example, coroutine switching can be implemented through the following steps:

  • Using <span>save</span> instructions to save the current coroutine’s stack, registers, and other states into a user-space structure.

  • Using <span>restore</span> instructions to restore the state from the target coroutine’s structure, jumping to the target coroutine’s location.

4. Advantages and Challenges of Coroutine Switching

Advantages:

  1. Lightweight: Coroutines are lighter execution units than threads, and switching does not involve kernel-level operations, thus avoiding overhead from switching between kernel and user modes.

  2. Low Overhead: Coroutine context switching only involves saving and restoring a small number of registers and stack, making it much more efficient than thread context switching.

  3. Flexibility: Coroutine scheduling is entirely user-controlled, allowing for non-preemptive scheduling, where coroutines voluntarily yield the CPU, avoiding frequent context switching.

Challenges:

  1. Scheduling Complexity: Since coroutine scheduling is controlled by user-space programs, careful management of each coroutine’s state is necessary to avoid deadlocks, resource contention, and other issues.

  2. Cooperative Scheduling: Coroutine scheduling is typically cooperative (coroutines voluntarily yield control), and if a coroutine fails to yield control correctly, it may lead to program blocking or endless loops.

5. Use Cases

Coroutine switching is particularly suitable for the following scenarios:

  • High Concurrency I/O Intensive Tasks: For example, network programming, file operations, etc., where multiple coroutines can execute within the same thread, avoiding the overhead of thread switching.

  • Cooperative Scheduling: Such as game engines, graphics rendering, etc., where efficient management of multiple tasks is required, coroutines can provide a simpler control flow.

  • Lightweight Task Concurrency: When there are many tasks, but each task has a very short execution time, coroutines provide a more efficient concurrency model than threads.

Conclusion

In Linux, coroutine switching is typically user-level, not involving kernel operations, thus not causing context switching at the operating system level. Coroutine switching is managed by user-space libraries, usually implemented by saving and restoring registers, stack pointers, and other states. Compared to threads, the overhead of coroutine switching is very small, making it suitable for high-concurrency I/O intensive tasks. The most common coroutine implementations include context switching based on <span>setjmp/longjmp</span>, <span>ucontext</span>, or assembly language.

Leave a Comment