深入浅出 协程(Coroutine):从原理到实践

文章目录
- [深入浅出 协程(Coroutine):从原理到实践](#深入浅出 协程(Coroutine):从原理到实践)
-
- [1. 什么是协程?](#1. 什么是协程?)
- [2. 协程的核心特点](#2. 协程的核心特点)
-
- [2.1 协作式调度](#2.1 协作式调度)
- [2.2 极轻量级](#2.2 极轻量级)
- [2.3 极高的并发能力](#2.3 极高的并发能力)
- [3. 协程 vs 线程 vs 函数](#3. 协程 vs 线程 vs 函数)
- [4. 协程的工作原理:挂起与恢复](#4. 协程的工作原理:挂起与恢复)
- [5. 协程的两种主要形式](#5. 协程的两种主要形式)
-
- [5.1 有栈协程](#5.1 有栈协程)
- [5.2 无栈协程](#5.2 无栈协程)
- [6. 实际应用场景](#6. 实际应用场景)
- [7. 主流语言中的协程实现](#7. 主流语言中的协程实现)
- [8. 协程的局限性](#8. 协程的局限性)
- [9. 各语言协程代码示例](#9. 各语言协程代码示例)
-
- [9.1 Go(有栈协程)](#9.1 Go(有栈协程))
- [9.2 Python(asyncio 无栈协程)](#9.2 Python(asyncio 无栈协程))
- [9.3 JavaScript / TypeScript(async/await + Promise)](#9.3 JavaScript / TypeScript(async/await + Promise))
- [9.4 Rust(async/await + tokio)](#9.4 Rust(async/await + tokio))
- [9.5 C++20(无栈协程)](#9.5 C++20(无栈协程))
- 9.6 C#(async/await)
- [9.7 Lua(有栈协程)](#9.7 Lua(有栈协程))
- [9.8 Java 21(虚拟线程 / 有栈协程)](#9.8 Java 21(虚拟线程 / 有栈协程))
- [9.9 Kotlin(协程库 / 无栈协程)](#9.9 Kotlin(协程库 / 无栈协程))
- [10. 总结:协程与线程如何协同](#10. 总结:协程与线程如何协同)
1. 什么是协程?
协程(Coroutine) 是一种比线程更轻量级的并发编程模型。它允许在同一个线程内拥有多个执行流,这些执行流可以像函数一样被调用和挂起,但又能多次恢复执行,因此也被称为"可暂停和恢复的函数"。
与传统的函数"一次调用、一次返回"不同,协程可以在执行中途主动让出 CPU,等待某个条件满足后再从让出的位置继续执行。
2. 协程的核心特点
2.1 协作式调度
- 线程 采用抢占式调度:操作系统内核可以在任意时刻暂停一个正在运行的线程,将 CPU 时间片分配给另一个线程。线程不知道自己何时会被暂停,因此需要使用锁、信号量等机制保护共享数据。
- 协程 采用协作式调度 :协程主动通过
await、yield等操作告知调度器"我要等待某个操作(如 I/O、定时器),请先运行其他协程"。协程只在明确的让出点发生切换,避免了非预期的数据竞争,通常不需要使用锁。
2.2 极轻量级
- 线程由操作系统内核管理,创建和切换需要陷入内核态,开销较大。一个线程默认栈大小通常在 MB 级别,因此单个进程能创建的线程数量有限(一般几千个)。
- 协程由用户态的运行时(如编程语言的协程库)管理,创建和切换只涉及少量用户态指令,开销极低。一个协程的栈可以小到 KB 甚至几十字节。一个线程可以轻松创建数十万甚至上百万个协程。
2.3 极高的并发能力
由于协程足够轻量,单个线程可以管理海量并发任务。例如一个网络服务器可以为每个客户端连接创建一个协程,当协程等待网络数据时主动挂起,线程去处理其他就绪协程。这种模型避免了多线程上下文切换和内存占用,能实现极高的 I/O 并发吞吐量。
3. 协程 vs 线程 vs 函数
| 特性 | 函数 | 协程 | 线程 |
|---|---|---|---|
| 调度方式 | 调用者决定(无调度) | 协作式(用户主动让出) | 抢占式(操作系统强制) |
| 执行流 | 单次执行,一次返回 | 可多次挂起/恢复 | 独立的并发执行流 |
| 资源开销 | 极小(仅栈帧) | 小(用户态管理) | 大(内核态管理,栈大) |
| 数据同步 | 无竞争 | 几乎无竞争(在让出点同步) | 竞争激烈,需要锁、信号量 |
| 数量上限 | 无限制(受内存限制) | 极高(数十万/百万) | 低(几千) |
| 典型应用 | 封装计算逻辑 | 高并发 I/O、流式处理 | CPU 密集型计算、多核利用 |
4. 协程的工作原理:挂起与恢复
协程的核心机制是 挂起(suspend) 和 恢复(resume)。
- 协程执行到某个挂起点,调用一个特殊的挂起函数(如
await),表示需要等待某条件(例如网络数据返回、定时器到期)。 - 此时,协程会保存当前的执行状态(程序计数器、局部变量、栈指针等)到堆内存中,然后让出线程的 CPU 执行权。
- 线程调度器会从就绪队列中取出另一个协程运行。
- 当之前等待的条件满足时(例如数据到达),调度器会恢复挂起的协程:重新加载其保存的状态,从挂起点继续执行。
生活化类比:厨师与订单
假设你是一个厨师(线程 ),需要同时处理多个订单(协程)。
- 线程模式:给每个订单安排一个专属副手(线程),副手站在锅前等水烧开(阻塞)。1000 个订单就需要 1000 个副手,厨房根本装不下(资源耗尽)。
- 协程模式 :你一个人处理所有订单。开始第一单,烧上水,不等水开就挂起 这一单,去切第二单的菜;切完菜,第一单的水还没开,又去处理第三单。水烧开时,你会收到通知,然后恢复第一单继续烹饪。一个人同时推进所有订单,效率极高。
5. 协程的两种主要形式
5.1 有栈协程
- 每个协程拥有独立的调用栈,可以像线程一样在任意嵌套函数中挂起。从使用角度看,它非常像一个"轻量级线程"。
- 代表语言/实现:Go 的 goroutine、Lua 的 coroutine。
- 优点:使用方便,任意函数都可以挂起,不需要特殊语法标记。
- 缺点:实现较复杂,内存占用相对较高(但依然远小于线程)。
5.2 无栈协程
- 协程没有独立的调用栈,状态保存在堆上的对象中。只能在标记为
async的函数内部使用await来挂起。 - 代表语言/实现 :C++20 协程、Python
asyncio、Rustasync/await、JavaScriptasync/await、C#async/await。 - 优点:极轻量,零开销抽象,适合与现有同步代码集成。
- 缺点 :具有"传染性"------调用
async函数的地方通常也需要是async函数,可能导致代码重构成本。
6. 实际应用场景
- 高并发网络服务:Web 服务器、API 网关、反向代理(如 Nginx 早期协程思想、OpenResty/Lua、Python Tornado)。
- 微服务与网关:处理大量长连接(WebSocket)、服务间 RPC 调用。
- I/O 密集型任务:文件读写、数据库查询、缓存访问。协程让 CPU 在等待 I/O 时能够处理其他任务。
- GUI 和游戏编程:处理用户输入、动画、网络请求。协程可以优雅实现延迟执行、顺序动画。
- 流式数据处理:生产者-消费者模型,使用协程作为管道,一边生产一边消费。
7. 主流语言中的协程实现
| 语言 | 实现方式 | 栈类型 |
|---|---|---|
| Go | 语言原生支持 go 关键字启动 goroutine,运行时调度器成熟 |
有栈 |
| Python | asyncio 库 + async/await 语法,基于事件循环 |
无栈 |
| JavaScript/TypeScript | async/await + Promise,Node.js 高并发核心 |
无栈 |
| Rust | async/await + 生态运行时(如 tokio),零成本抽象 |
无栈 |
| C++ | C++20 标准引入 co_await、co_yield、co_return |
无栈 |
| C# | 早期就引入 async/await,非常成熟 |
无栈 |
| Lua | coroutine 库支持,轻巧强大 |
有栈 |
| Java | Java 21 开始正式引入了 虚拟线程(Virtual Threads) | 有栈 |
| Kotlin | 协程库(kotlinx.coroutines)+launch、async |
无栈 |
8. 协程的局限性
- 不能利用多核:单个线程内的协程是并发而非并行。要利用多核 CPU,需要配合多线程模型(例如每个 CPU 核心启动一个线程,每个线程内运行协程)。
- 阻塞操作的影响 :如果一个协程发起阻塞系统调用(如
time.sleep(10)),它会阻塞整个线程,导致该线程上的所有协程都无法运行。因此协程环境要求所有 I/O 操作都是非阻塞的。 - 调试难度:协程的挂起和恢复会使调用栈变得不连续,调试时可能看到不完整的栈信息,增加问题定位难度。
- 传染性(无栈协程) :
async函数会"污染"调用它的代码,需要全链条异步化,对既有代码库改造有一定成本。
9. 各语言协程代码示例
下面通过具体代码演示不同语言中协程(或异步任务)的基本用法。所有示例均展示如何创建协程、执行异步等待以及并发运行多个任务。
9.1 Go(有栈协程)
Go 的 goroutine 配合 channel 实现通信,语言运行时自动调度。
go
package main
import (
"fmt"
"time"
)
// 模拟一个异步任务
func asyncTask(name string, duration time.Duration) {
time.Sleep(duration) // 模拟 I/O 操作
fmt.Println(name, "完成")
}
func main() {
// 启动两个 goroutine(协程)
go asyncTask("任务A", 2*time.Second)
go asyncTask("任务B", 1*time.Second)
// 等待足够时间让协程执行完毕(生产环境常用 sync.WaitGroup)
time.Sleep(3 * time.Second)
fmt.Println("主函数结束")
}
9.2 Python(asyncio 无栈协程)
使用 async/await 配合 asyncio 事件循环。
python
import asyncio
# 定义一个异步协程
async def async_task(name, delay):
print(f"{name} 开始")
await asyncio.sleep(delay) # 模拟异步 I/O,主动挂起
print(f"{name} 完成")
async def main():
# 并发执行两个协程
task1 = asyncio.create_task(async_task("任务A", 2))
task2 = asyncio.create_task(async_task("任务B", 1))
await task1
await task2
asyncio.run(main())
9.3 JavaScript / TypeScript(async/await + Promise)
基于 Promise 的异步模型,事件循环由 JavaScript 运行时(如 Node.js、浏览器)提供。
javascript
// 模拟异步操作(例如网络请求)
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function asyncTask(name, duration) {
console.log(`${name} 开始`);
await delay(duration); // 挂起,等待定时器完成
console.log(`${name} 完成`);
}
async function main() {
// 并发执行两个异步任务
const promiseA = asyncTask("任务A", 2000);
const promiseB = asyncTask("任务B", 1000);
await promiseA;
await promiseB;
}
main();
9.4 Rust(async/await + tokio)
Rust 使用 async/await 语法,但需要配合外部异步运行时(如 tokio)。
rust
use tokio::time::{sleep, Duration};
// 异步函数
async fn async_task(name: &str, duration_secs: u64) {
println!("{} 开始", name);
sleep(Duration::from_secs(duration_secs)).await; // 异步等待,挂起
println!("{} 完成", name);
}
#[tokio::main]
async fn main() {
// 并发执行两个异步任务
let task_a = async_task("任务A", 2);
let task_b = async_task("任务B", 1);
tokio::join!(task_a, task_b);
}
9.5 C++20(无栈协程)
C++20 协程相对底层,下面展示一个简单的生成器 (generator),利用 co_yield 挂起并返回值。
cpp
#include <iostream>
#include <coroutine>
#include <memory>
// 一个简单的生成器类型
template<typename T>
struct Generator {
struct promise_type {
T current_value;
Generator get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
// 协程创建后立即挂起,需要首次 resume() 才开始执行。
std::suspend_always initial_suspend() { return {}; }
//协程结束后挂起,不自动销毁协程帧,由 Generator 析构函数手动销毁。
std::suspend_always final_suspend() noexcept { return {}; }
// 遇到未处理异常后自动终止。注意:任何协程体内的异常都会导致程序崩溃。实际项目中应考虑通过 promise 存储异常并在 next() 中重新抛出。
void unhandled_exception() { std::terminate(); }
// 每次 co_yield 后挂起,保存当前值,等待下一次 resume()。
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
// 协程正常结束(无返回值)。
void return_void() {}
};
// 定义句柄
std::coroutine_handle<promise_type> handle;
// 构造函数
Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
// final_suspend 返回 suspend_always 意味着协程结束后不会自动清理,必须手动调用。析构函数做了这件事,所以是安全的
~Generator() { if (handle) handle.destroy(); }
// next函数:若句柄有效,调用 resume() 执行协程直到下一个挂起点或结束。
bool next() { return handle ? (handle.resume(), !handle.done()) : false; } // 返回 !handle.done(); 若协程还在挂起状态(刚 yield 完)→ true;若协程已结束(final_suspend 后)→ false
// 直接返回 promise 中保存的 current_value
T value() const { return handle.promise().current_value; }
};
// 一个协程:生成斐波那契数列
Generator<int> fibonacci(int n) {
int a = 0, b = 1;
for (int i = 0; i < n; ++i) {
co_yield a; // 挂起并返回当前值
int temp = a;
a = b;
b = temp + b;
}
}
int main() {
auto gen = fibonacci(10);
// 用户通过 next() 推进协程,通过 value() 获取当前值。
while (gen.next()) {
std::cout << gen.value() << " ";
}
std::cout << std::endl;
return 0;
}
9.6 C#(async/await)
C# 的 async/await 模型成熟且与 .NET 运行时深度集成。
csharp
using System;
using System.Threading.Tasks;
class Program
{
// 模拟异步操作
static async Task AsyncTask(string name, int delayMs)
{
Console.WriteLine($"{name} 开始");
await Task.Delay(delayMs); // 异步等待,挂起协程
Console.WriteLine($"{name} 完成");
}
static async Task Main()
{
// 并发执行两个异步任务
Task taskA = AsyncTask("任务A", 2000);
Task taskB = AsyncTask("任务B", 1000);
await Task.WhenAll(taskA, taskB);
}
}
9.7 Lua(有栈协程)
Lua 的协程通过 coroutine.create、resume 和 yield 实现显式的挂起与恢复。
lua
-- 定义一个协程函数
function async_task(name, duration)
print(name .. " 开始")
-- 模拟异步等待:使用 socket.sleep 或这里用循环简单示意
-- 实际中会调用一个非阻塞的等待,并通过 yield 让出
coroutine.yield() -- 挂起点,模拟等待
print(name .. " 完成")
end
-- 创建协程
local co1 = coroutine.create(async_task)
local co2 = coroutine.create(async_task)
-- 启动协程(首次执行到第一个 yield)
coroutine.resume(co1, "任务A")
coroutine.resume(co2, "任务B")
-- 模拟等待条件满足后恢复(例如定时器触发)
-- 实际生产环境由调度器管理,此处仅演示恢复
coroutine.resume(co1)
coroutine.resume(co2)
说明 :Lua 示例简化了异步等待的模拟,真实场景中通常会结合回调或事件循环来触发
resume。
9.8 Java 21(虚拟线程 / 有栈协程)
Java 虚拟线程由 JVM 管理,创建和切换开销极低,支持百万级并发。使用 Thread.startVirtualThread() 或 Executors.newVirtualThreadPerTaskExecutor()。
java
import java.time.Duration;
import java.util.concurrent.Executors;
public class VirtualThreadsDemo {
// 模拟异步任务(实际为阻塞操作,但虚拟线程可高效挂起)
static void asyncTask(String name, int durationSeconds) {
System.out.println(name + " 开始,运行于:" + Thread.currentThread());
try {
Thread.sleep(Duration.ofSeconds(durationSeconds)); // 阻塞,虚拟线程会自动让出底层载体线程
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(name + " 完成");
}
public static void main(String[] args) throws InterruptedException {
// 方式1:直接启动虚拟线程
var t1 = Thread.startVirtualThread(() -> asyncTask("任务A", 2));
var t2 = Thread.startVirtualThread(() -> asyncTask("任务B", 1));
t1.join();
t2.join();
// 方式2:使用虚拟线程执行器,适合大量任务
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> asyncTask("任务C", 1));
executor.submit(() -> asyncTask("任务D", 2));
} // 自动关闭并等待所有任务完成
System.out.println("主函数结束");
}
}
说明 :Java 虚拟线程虽然语法上看起来像普通线程,但底层由 JVM 实现了用户态调度,阻塞操作(如
Thread.sleep、Socket.read)会自动挂起虚拟线程,不会阻塞操作系统线程,因此可以轻松创建数十万甚至百万个虚拟线程。
9.9 Kotlin(协程库 / 无栈协程)
kotlin
import kotlinx.coroutines.*
suspend fun asyncTask(name: String, delayMs: Long) {
println("$name 开始")
delay(delayMs) // 挂起函数,不会阻塞线程
println("$name 完成")
}
fun main() = runBlocking {
// 并发执行两个协程
val job1 = launch { asyncTask("任务A", 2000) }
val job2 = launch { asyncTask("任务B", 1000) }
job1.join()
job2.join()
// 使用 async 返回结果
val result = async {
delay(500)
"计算结果"
}
println(result.await())
}
说明 :Kotlin 通过协程库(
kotlinx.coroutines)提供无栈协程,使用launch、async等构建器,语法简洁。
10. 总结:协程与线程如何协同
协程并不是要取代线程 ,而是与线程协同工作:
- 协程负责高并发、多任务调度(逻辑流管理),提供轻量级的任务切换。
- 线程负责真正利用多核 CPU(物理执行),同时作为协程的载体。
一种常见架构是:启动与 CPU 核心数量相等的线程,每个线程内运行一个事件循环和成千上万个协程。这样既能充分利用多核,又能轻松支撑百万级并发连接。
理解协程,只需抓住四个字:挂起、恢复。它是一种优雅而强大的并发抽象,正越来越多地被现代编程语言和框架采纳。掌握协程,将帮助你写出更高性能、更易维护的 I/O 密集型程序。
本文基于协程的核心思想"主动挂起、恢复执行"展开,从概念、原理、对比、实现到应用场景,全方位介绍了协程。希望能帮助读者建立对协程的系统认识,并在实际开发中合理运用。