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

深入浅出 协程(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 时间片分配给另一个线程。线程不知道自己何时会被暂停,因此需要使用锁、信号量等机制保护共享数据。
  • 协程 采用协作式调度 :协程主动通过 awaityield 等操作告知调度器"我要等待某个操作(如 I/O、定时器),请先运行其他协程"。协程只在明确的让出点发生切换,避免了非预期的数据竞争,通常不需要使用锁。

2.2 极轻量级

  • 线程由操作系统内核管理,创建和切换需要陷入内核态,开销较大。一个线程默认栈大小通常在 MB 级别,因此单个进程能创建的线程数量有限(一般几千个)。
  • 协程由用户态的运行时(如编程语言的协程库)管理,创建和切换只涉及少量用户态指令,开销极低。一个协程的栈可以小到 KB 甚至几十字节。一个线程可以轻松创建数十万甚至上百万个协程。

2.3 极高的并发能力

由于协程足够轻量,单个线程可以管理海量并发任务。例如一个网络服务器可以为每个客户端连接创建一个协程,当协程等待网络数据时主动挂起,线程去处理其他就绪协程。这种模型避免了多线程上下文切换和内存占用,能实现极高的 I/O 并发吞吐量。

3. 协程 vs 线程 vs 函数

特性 函数 协程 线程
调度方式 调用者决定(无调度) 协作式(用户主动让出) 抢占式(操作系统强制)
执行流 单次执行,一次返回 可多次挂起/恢复 独立的并发执行流
资源开销 极小(仅栈帧) (用户态管理) (内核态管理,栈大)
数据同步 无竞争 几乎无竞争(在让出点同步) 竞争激烈,需要锁、信号量
数量上限 无限制(受内存限制) 极高(数十万/百万) (几千)
典型应用 封装计算逻辑 高并发 I/O、流式处理 CPU 密集型计算、多核利用

4. 协程的工作原理:挂起与恢复

协程的核心机制是 挂起(suspend)恢复(resume)

  1. 协程执行到某个挂起点,调用一个特殊的挂起函数(如 await),表示需要等待某条件(例如网络数据返回、定时器到期)。
  2. 此时,协程会保存当前的执行状态(程序计数器、局部变量、栈指针等)到堆内存中,然后让出线程的 CPU 执行权。
  3. 线程调度器会从就绪队列中取出另一个协程运行。
  4. 当之前等待的条件满足时(例如数据到达),调度器会恢复挂起的协程:重新加载其保存的状态,从挂起点继续执行。

生活化类比:厨师与订单

假设你是一个厨师(线程 ),需要同时处理多个订单(协程)。

  • 线程模式:给每个订单安排一个专属副手(线程),副手站在锅前等水烧开(阻塞)。1000 个订单就需要 1000 个副手,厨房根本装不下(资源耗尽)。
  • 协程模式 :你一个人处理所有订单。开始第一单,烧上水,不等水开就挂起 这一单,去切第二单的菜;切完菜,第一单的水还没开,又去处理第三单。水烧开时,你会收到通知,然后恢复第一单继续烹饪。一个人同时推进所有订单,效率极高。

5. 协程的两种主要形式

5.1 有栈协程

  • 每个协程拥有独立的调用栈,可以像线程一样在任意嵌套函数中挂起。从使用角度看,它非常像一个"轻量级线程"。
  • 代表语言/实现:Go 的 goroutine、Lua 的 coroutine。
  • 优点:使用方便,任意函数都可以挂起,不需要特殊语法标记。
  • 缺点:实现较复杂,内存占用相对较高(但依然远小于线程)。

5.2 无栈协程

  • 协程没有独立的调用栈,状态保存在堆上的对象中。只能在标记为 async 的函数内部使用 await 来挂起。
  • 代表语言/实现 :C++20 协程、Python asyncio、Rust async/await、JavaScript async/await、C# async/await
  • 优点:极轻量,零开销抽象,适合与现有同步代码集成。
  • 缺点 :具有"传染性"------调用 async 函数的地方通常也需要是 async 函数,可能导致代码重构成本。

6. 实际应用场景

  1. 高并发网络服务:Web 服务器、API 网关、反向代理(如 Nginx 早期协程思想、OpenResty/Lua、Python Tornado)。
  2. 微服务与网关:处理大量长连接(WebSocket)、服务间 RPC 调用。
  3. I/O 密集型任务:文件读写、数据库查询、缓存访问。协程让 CPU 在等待 I/O 时能够处理其他任务。
  4. GUI 和游戏编程:处理用户输入、动画、网络请求。协程可以优雅实现延迟执行、顺序动画。
  5. 流式数据处理:生产者-消费者模型,使用协程作为管道,一边生产一边消费。

7. 主流语言中的协程实现

语言 实现方式 栈类型
Go 语言原生支持 go 关键字启动 goroutine,运行时调度器成熟 有栈
Python asyncio 库 + async/await 语法,基于事件循环 无栈
JavaScript/TypeScript async/await + Promise,Node.js 高并发核心 无栈
Rust async/await + 生态运行时(如 tokio),零成本抽象 无栈
C++ C++20 标准引入 co_awaitco_yieldco_return 无栈
C# 早期就引入 async/await,非常成熟 无栈
Lua coroutine 库支持,轻巧强大 有栈
Java Java 21 开始正式引入了 虚拟线程(Virtual Threads) 有栈
Kotlin 协程库(kotlinx.coroutines)+launchasync 无栈

8. 协程的局限性

  1. 不能利用多核:单个线程内的协程是并发而非并行。要利用多核 CPU,需要配合多线程模型(例如每个 CPU 核心启动一个线程,每个线程内运行协程)。
  2. 阻塞操作的影响 :如果一个协程发起阻塞系统调用(如 time.sleep(10)),它会阻塞整个线程,导致该线程上的所有协程都无法运行。因此协程环境要求所有 I/O 操作都是非阻塞的。
  3. 调试难度:协程的挂起和恢复会使调用栈变得不连续,调试时可能看到不完整的栈信息,增加问题定位难度。
  4. 传染性(无栈协程)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.createresumeyield 实现显式的挂起与恢复。

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.sleepSocket.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)提供无栈协程,使用 launchasync 等构建器,语法简洁。

10. 总结:协程与线程如何协同

协程并不是要取代线程 ,而是与线程协同工作

  • 协程负责高并发、多任务调度(逻辑流管理),提供轻量级的任务切换。
  • 线程负责真正利用多核 CPU(物理执行),同时作为协程的载体。

一种常见架构是:启动与 CPU 核心数量相等的线程,每个线程内运行一个事件循环和成千上万个协程。这样既能充分利用多核,又能轻松支撑百万级并发连接。

理解协程,只需抓住四个字:挂起、恢复。它是一种优雅而强大的并发抽象,正越来越多地被现代编程语言和框架采纳。掌握协程,将帮助你写出更高性能、更易维护的 I/O 密集型程序。


本文基于协程的核心思想"主动挂起、恢复执行"展开,从概念、原理、对比、实现到应用场景,全方位介绍了协程。希望能帮助读者建立对协程的系统认识,并在实际开发中合理运用。

相关推荐
2401_892070981 天前
【Linux C++ 后端实战】异步日志系统 AsyncLogging 完整设计与源码解析
linux·c++·高并发·异步日志
丁劲犇2 天前
QMetaObject的invokeMethod异步阻塞调用在MCPServer开发中的巧妙应用
qt·ai·agent·异步·阻塞·mcp·mcp server
叫我一声阿雷吧2 天前
JS 入门通关手册(43):async/await 原理与异常处理(实战 + 面试,彻底搞懂)
javascript·异常处理·promise·前端面试·async/await·generator·异步编程
冰河团队3 天前
一个拉胯的分库分表方案有多绝望?整个部门都在救火!
java·高并发·分布式数据库·分库分表·高性能
Rick19933 天前
Java 接口高并发优化方案
java·性能优化·高并发
牧魂.4 天前
MySQL 主从延迟根因诊断法
mysql·高并发·主从复制·主从延迟·数据库调优
zztfj4 天前
C# 异步方法 async / await CancellationToken 设置任务超时并手动取消耗时处理
c#·异步
__土块__6 天前
一次电商秒杀系统架构评审:从本地锁到分布式锁的演进与取舍
java·redis·高并发·分布式锁·redisson·架构设计·秒杀系统
罗山仔6 天前
【Vertx构建异步响应式reactive mybatis,mybatis-vertx-adaptor】
mybatis·orm·异步·reactive·响应式·webflux·vertx