对于C++程序员来说,线程的概念应该都很清楚。但是协程和纤程的概念,可能还有不少人弄不清楚。今天这篇文章回应网友关于纤程和协程的问题,如果你也不清楚什么是纤程和协程,那么一定要仔细阅读这篇文章。

线程的起源与局限性
要理解纤程和协程,首先我们得理解线程。在计算机发展的早期阶段,程序执行采用的是严格的单线程执行模式。这种模式下,CPU一次只能执行一条指令,程序必须按照编写的顺序一步一步地执行,不能跳跃或并行处理。
以下是一个早期单线程程序的典型结构:
int main() {
// 步骤1:初始化
initialize_system();
// 步骤2:处理输入
input = read_user_input();
// 步骤3:处理数据
result = process_data(input);
// 步骤4:输出结果
print_result(result);
// 步骤5:清理资源
cleanup();
return 0;
}
这样的程序优点很明显,程序的执行路径完全可预测,出现BUG也很容易定位和重现。但是在这种单线程模式下,当程序遇到耗时的I/O操作(例如网络通信、用户输入等)时,整个程序会被阻塞,CPU只能等待操作完成。
于是,便有了多线程。在多线程模式下,多条线程分别独立执行不同的任务,互不影响,其中一条线程阻塞,另一条线程仍然可以正常处理请求。
多线程的挑战
虽然多线程解决了单线程的许多问题,但也引入了新的问题。例如C10K问题,C10K概念最早由 Dan Kegel 在 1999 年提出,指在服务器端如何高效处理同时一万个并发连接的技术挑战。在早期的服务器网络模型中,通常的做法是每连接由一个线程处理,当连接数达到几千甚至一万时,就会出现线程的内存占用过大(10,000 × 1MB = 10GB)以及线程调度开销过大等问题。
另外,在多线程之间共享数据时,如何避免冲突、在保持一致性的同时又不牺牲性能,这使得代码逻辑变得异常复杂。此外,异步编程中使用回调函数处理事件导致层层回调嵌套,从而使代码非常难以阅读和维护。
协程的引入
在1958年左右,一位美国的程序员Melvin Conway在他的论文《Design of a Separable Transition-diagram Compiler》中提出了协程的概念。没错,软件工程中著名的《康威定律》就是他提出来的。

协程(Coroutine)是一种用户态的轻量级线程,它允许在一个线程中并发执行多个任务,且任务之间可以主动让出控制权,在适当的时机继续执行。通俗地讲,协程就是你可以在一个函数运行到一半时“暂停”,去做别的事,过一会儿再从上次暂停的地方“继续”。
从C++20开始,C++引入了co_await
、co_yield
、co_return
关键字来加入协程的支持。所以协程是C++语言层面提供的语法支持,它们就像if
、for
一样,是语言的一部分,具体是由编译器来实现的机制。
具体来说,这些关键字本身并不“做任何事情”。编译器看到你写了co_
开头的关键词后,知道你在写协程函数,就会自动生成一个复杂的状态机类,这个状态机类会记住每一次co_yield
的暂停点以及恢复时的跳转逻辑。
下面我们通过代码来了解C++中协程的使用:
#include <iostream>
#include <coroutine>
#include <memory>
struct Generator {
struct promise_type {
int current_value;
std::suspend_always yield_value(int value) {
current_value = value;
return {};
}
std::suspend_never return_void() { return {}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
Generator get_return_object() { return Generator{this}; }
};
explicit Generator(promise_type* p)
: handle(std::coroutine_handle<promise_type>::from_promise(*p)) {}
~Generator() { if (handle) handle.destroy(); }
bool next() {
if (!handle || handle.done()) return false;
handle.resume();
return !handle.done();
}
int current() const {
return handle.promise().current_value;
}
private:
std::coroutine_handle<promise_type> handle;
};
// 协程函数
Generator generate_numbers() {
for (int i = 0; i < 3; ++i) {
co_yield i; // 暂停并返回i
}
}
int main() {
Generator gen = generate_numbers();
while (gen.next()) {
std::cout << "Generated: " << gen.current() << std::endl;
}
return 0;
}
代码解析
这个示例程序使用协程来实现一个生成器,每次调用next()
都会生成一个新的整数。Generator
是一个结构体,它是协程的返回类型,负责协程的管理,例如恢复执行、销毁、取值等等。嵌套的promise_type
结构体是每一个协程返回类型必须实现的结构体,它是C++20协程机制的核心组成部分,协程编译器通过它来控制协程的创建、挂起、恢复、返回等行为。promise_type
可以看作是编译器和你“沟通协作”的桥梁。
我们来看看promise_type
的成员函数:
yield_value
:当协程中执行co_yield value
时被调用,它会暂停协程,并把value
保存到current_value
中。return_void
:协程正常结束时调用,其返回值suspend_never
表示不暂停协程,立即结束。initial_suspend
:协程刚创建时的逻辑,这里suspend_always
表示协程创建时就暂停。final_suspend
:返回suspend_always
是指协程结束后进入暂停,等待销毁。unhandled_exception
:协程内部异常未处理时调用,这里调用terminate
终止程序。get_return_object
:返回协程接口对象Generator
,从而将promise
和coroutine_handle
关联起来。
Generator
的构造函数从promise
对象构造coroutine_handle
,~Generator()
析构函数用于销毁协程handle
,释放资源。next
函数用于继续执行协程,它调用handle.resume
来恢复协程的运行,使协程函数从上一次调用co_yield
之后继续运行,直到下一个co_yield
调用。current
函数用于获取当前由co_yield
返回的值。
在generate_numbers
协程函数中,编译器看到使用了co_yield
,因此会将此函数编译为协程函数,自动生成一个复杂的状态机类。每次运行到co_yield
时,函数会暂停执行,保存当前值,同时将控制权返回给调用者,下一次调用next()
会从co_yield
后继续执行。
在main
函数中,Generator gen = generate_numbers();
创建了协程,但因为promise_type
中的initial_suspend
是suspend_always
,所以协程创建后立即暂停执行。每次调用gen.next()
时,协程才会恢复执行。代码运行后,控制台输出:
Generated: 0
Generated: 1
Generated: 2
因此,协程就像是一个支持“断点保存”的函数,每次执行到co_yield
就被“暂停保存”,并返回一个值。下一次继续执行协程时,会从上次的断点处接着运行。
纤程的介绍
纤程(Fiber)是一种轻量级的协作式线程,拥有很多与协程相似的特性。例如,它们都采用协作式调度模型,都需要主动让出控制权才能切换到其他执行单元,都避免了抢占式调度带来的竞态条件问题,都具有轻量级特性并解决了异步编程复杂的问题。
但是,纤程是操作系统级别的实现,其功能通过系统API提供,而协程通常只是语言级别或库级别的实现。纤程完全由程序员手动调度,而不得不
System: 程,而协程可能提供更智能的调度机制,例如Go语言的调度器就支持自动调度。
纤程与协程的比较
有网友可能会问,协程和纤程的差别似乎不大,既然协程已经非常强大,为什么还需要纤程?别急,我们继续探讨。
纤程最早由Windows系统在Windows NT 3.51(1995年)中提出并实现。相比之下,现代编程语言支持协程的时间较晚,例如:
- Lua 在2003年引入协程;
- Python 在2006年支持协程;
- Go 在2009年发布时通过goroutine提供了轻量级协程支持;
- C++ 直到2020年的C++20版本才支持协程。
在Linux/Unix系统中,虽然没有直接的纤程API,但可以通过ucontext
系列函数实现类似纤程的功能。Boost Fiber库则提供了现代纤程的跨平台实现。
纤程代码示例
下面是一个使用 Boost Fiber 实现的简单纤程示例:
#include <iostream>
#include <boost/fiber/all.hpp>
void task1() {
for (int i = 0; i < 3; ++i) {
std::cout << "Task 1: " << i << std::endl;
boost::this_fiber::yield(); // 让出控制权
}
}
void task2() {
for (int i = 0; i < 3; ++i) {
std::cout << "Task 2: " << i << std::endl;
boost::this_fiber::yield(); // 让出控制权
}
}
int main() {
boost::fibers::fiber f1(task1); // 创建纤程1
boost::fibers::fiber f2(task2); // 创建纤程2
f1.join(); // 等待纤程1完成
f2.join(); // 等待纤程2完成
std::cout << "All tasks completed." << std::endl;
return 0;
}
代码解析
在这个示例中,main
函数使用 Boost Fiber 创建了两个纤程。两个纤程的逻辑相同:循环三次,打印输出,然后调用yield
让出控制权。例如,task1
让出控制权后进入暂停状态,Boost Fiber 会自动调度task2
执行。当task2
让出控制权后,task1
会在之前暂停的代码处继续运行。
纤程和协程确实非常相似。纤程是 Windows 在协程尚未被各编程语言广泛支持时(1995年)提出的技术。如今,纤程更多被现代编程语言的协程机制所取代,但在需要细粒度控制执行流程的场景下,纤程仍然是一个很有价值的工具。
总结
协程和纤程都是轻量级的并发技术,解决了多线程编程中的许多问题。协程通过语言级支持(如C++20的co_await
、co_yield
、co_return
)提供更灵活的控制,而纤程依赖操作系统或库(如 Boost Fiber)实现,适合需要手动调度的场景。两者各有优势,程序员可以根据需求选择合适的技术。
发表回复