深入浅出 协程Coroutine从原理到实践文章目录深入浅出 协程Coroutine从原理到实践1. 什么是协程2. 协程的核心特点2.1 协作式调度2.2 极轻量级2.3 极高的并发能力3. 协程 vs 线程 vs 函数4. 协程的工作原理挂起与恢复生活化类比厨师与订单5. 协程的两种主要形式5.1 有栈协程5.2 无栈协程6. 实际应用场景7. 主流语言中的协程实现8. 协程的局限性9. 各语言协程代码示例9.1 Go有栈协程9.2 Pythonasyncio 无栈协程9.3 JavaScript / TypeScriptasync/await Promise9.4 Rustasync/await tokio9.5 C20无栈协程9.6 C#async/await9.7 Lua有栈协程9.8 Java 21虚拟线程 / 有栈协程9.9 Kotlin协程库 / 无栈协程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来挂起。代表语言/实现C20 协程、Pythonasyncio、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运行时调度器成熟有栈Pythonasyncio库 async/await语法基于事件循环无栈JavaScript/TypeScriptasync/await PromiseNode.js 高并发核心无栈Rustasync/await 生态运行时如tokio零成本抽象无栈CC20 标准引入co_await、co_yield、co_return无栈C#早期就引入async/await非常成熟无栈Luacoroutine库支持轻巧强大有栈JavaJava 21 开始正式引入了 虚拟线程Virtual Threads有栈Kotlin协程库kotlinx.coroutineslaunch、async无栈8. 协程的局限性不能利用多核单个线程内的协程是并发而非并行。要利用多核 CPU需要配合多线程模型例如每个 CPU 核心启动一个线程每个线程内运行协程。阻塞操作的影响如果一个协程发起阻塞系统调用如time.sleep(10)它会阻塞整个线程导致该线程上的所有协程都无法运行。因此协程环境要求所有 I/O 操作都是非阻塞的。调试难度协程的挂起和恢复会使调用栈变得不连续调试时可能看到不完整的栈信息增加问题定位难度。传染性无栈协程async函数会“污染”调用它的代码需要全链条异步化对既有代码库改造有一定成本。9. 各语言协程代码示例下面通过具体代码演示不同语言中协程或异步任务的基本用法。所有示例均展示如何创建协程、执行异步等待以及并发运行多个任务。9.1 Go有栈协程Go 的 goroutine 配合 channel 实现通信语言运行时自动调度。packagemainimport(fmttime)// 模拟一个异步任务funcasyncTask(namestring,duration time.Duration){time.Sleep(duration)// 模拟 I/O 操作fmt.Println(name,完成)}funcmain(){// 启动两个 goroutine协程goasyncTask(任务A,2*time.Second)goasyncTask(任务B,1*time.Second)// 等待足够时间让协程执行完毕生产环境常用 sync.WaitGrouptime.Sleep(3*time.Second)fmt.Println(主函数结束)}9.2 Pythonasyncio 无栈协程使用async/await配合asyncio事件循环。importasyncio# 定义一个异步协程asyncdefasync_task(name,delay):print(f{name}开始)awaitasyncio.sleep(delay)# 模拟异步 I/O主动挂起print(f{name}完成)asyncdefmain():# 并发执行两个协程task1asyncio.create_task(async_task(任务A,2))task2asyncio.create_task(async_task(任务B,1))awaittask1awaittask2 asyncio.run(main())9.3 JavaScript / TypeScriptasync/await Promise基于 Promise 的异步模型事件循环由 JavaScript 运行时如 Node.js、浏览器提供。// 模拟异步操作例如网络请求functiondelay(ms){returnnewPromise(resolvesetTimeout(resolve,ms));}asyncfunctionasyncTask(name,duration){console.log(${name}开始);awaitdelay(duration);// 挂起等待定时器完成console.log(${name}完成);}asyncfunctionmain(){// 并发执行两个异步任务constpromiseAasyncTask(任务A,2000);constpromiseBasyncTask(任务B,1000);awaitpromiseA;awaitpromiseB;}main();9.4 Rustasync/await tokioRust 使用async/await语法但需要配合外部异步运行时如tokio。usetokio::time::{sleep,Duration};// 异步函数asyncfnasync_task(name:str,duration_secs:u64){println!({} 开始,name);sleep(Duration::from_secs(duration_secs)).await;// 异步等待挂起println!({} 完成,name);}#[tokio::main]asyncfnmain(){// 并发执行两个异步任务lettask_aasync_task(任务A,2);lettask_basync_task(任务B,1);tokio::join!(task_a,task_b);}9.5 C20无栈协程C20 协程相对底层下面展示一个简单的生成器generator利用co_yield挂起并返回值。#includeiostream#includecoroutine#includememory// 一个简单的生成器类型templatetypenameTstructGenerator{structpromise_type{T current_value;Generatorget_return_object(){returnGenerator{std::coroutine_handlepromise_type::from_promise(*this)};}// 协程创建后立即挂起需要首次 resume() 才开始执行。std::suspend_alwaysinitial_suspend(){return{};}//协程结束后挂起不自动销毁协程帧由 Generator 析构函数手动销毁。std::suspend_alwaysfinal_suspend()noexcept{return{};}// 遇到未处理异常后自动终止。注意任何协程体内的异常都会导致程序崩溃。实际项目中应考虑通过 promise 存储异常并在 next() 中重新抛出。voidunhandled_exception(){std::terminate();}// 每次 co_yield 后挂起保存当前值等待下一次 resume()。std::suspend_alwaysyield_value(T value){current_valuevalue;return{};}// 协程正常结束无返回值。voidreturn_void(){}};// 定义句柄std::coroutine_handlepromise_typehandle;// 构造函数Generator(std::coroutine_handlepromise_typeh):handle(h){}// final_suspend 返回 suspend_always 意味着协程结束后不会自动清理必须手动调用。析构函数做了这件事所以是安全的~Generator(){if(handle)handle.destroy();}// next函数若句柄有效调用 resume() 执行协程直到下一个挂起点或结束。boolnext(){returnhandle?(handle.resume(),!handle.done()):false;}// 返回 !handle.done(); 若协程还在挂起状态刚 yield 完→ true若协程已结束final_suspend 后→ false// 直接返回 promise 中保存的 current_valueTvalue()const{returnhandle.promise().current_value;}};// 一个协程生成斐波那契数列Generatorintfibonacci(intn){inta0,b1;for(inti0;in;i){co_yielda;// 挂起并返回当前值inttempa;ab;btempb;}}intmain(){autogenfibonacci(10);// 用户通过 next() 推进协程通过 value() 获取当前值。while(gen.next()){std::coutgen.value() ;}std::coutstd::endl;return0;}9.6 C#async/awaitC# 的async/await模型成熟且与 .NET 运行时深度集成。usingSystem;usingSystem.Threading.Tasks;classProgram{// 模拟异步操作staticasyncTaskAsyncTask(stringname,intdelayMs){Console.WriteLine(${name}开始);awaitTask.Delay(delayMs);// 异步等待挂起协程Console.WriteLine(${name}完成);}staticasyncTaskMain(){// 并发执行两个异步任务TasktaskAAsyncTask(任务A,2000);TasktaskBAsyncTask(任务B,1000);awaitTask.WhenAll(taskA,taskB);}}9.7 Lua有栈协程Lua 的协程通过coroutine.create、resume和yield实现显式的挂起与恢复。-- 定义一个协程函数functionasync_task(name,duration)print(name.. 开始)-- 模拟异步等待使用 socket.sleep 或这里用循环简单示意-- 实际中会调用一个非阻塞的等待并通过 yield 让出coroutine.yield()-- 挂起点模拟等待print(name.. 完成)end-- 创建协程localco1coroutine.create(async_task)localco2coroutine.create(async_task)-- 启动协程首次执行到第一个 yieldcoroutine.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()。importjava.time.Duration;importjava.util.concurrent.Executors;publicclassVirtualThreadsDemo{// 模拟异步任务实际为阻塞操作但虚拟线程可高效挂起staticvoidasyncTask(Stringname,intdurationSeconds){System.out.println(name 开始运行于Thread.currentThread());try{Thread.sleep(Duration.ofSeconds(durationSeconds));// 阻塞虚拟线程会自动让出底层载体线程}catch(InterruptedExceptione){Thread.currentThread().interrupt();}System.out.println(name 完成);}publicstaticvoidmain(String[]args)throwsInterruptedException{// 方式1直接启动虚拟线程vart1Thread.startVirtualThread(()-asyncTask(任务A,2));vart2Thread.startVirtualThread(()-asyncTask(任务B,1));t1.join();t2.join();// 方式2使用虚拟线程执行器适合大量任务try(varexecutorExecutors.newVirtualThreadPerTaskExecutor()){executor.submit(()-asyncTask(任务C,1));executor.submit(()-asyncTask(任务D,2));}// 自动关闭并等待所有任务完成System.out.println(主函数结束);}}说明Java 虚拟线程虽然语法上看起来像普通线程但底层由 JVM 实现了用户态调度阻塞操作如Thread.sleep、Socket.read会自动挂起虚拟线程不会阻塞操作系统线程因此可以轻松创建数十万甚至百万个虚拟线程。9.9 Kotlin协程库 / 无栈协程importkotlinx.coroutines.*suspendfunasyncTask(name:String,delayMs:Long){println($name开始)delay(delayMs)// 挂起函数不会阻塞线程println($name完成)}funmain()runBlocking{// 并发执行两个协程valjob1launch{asyncTask(任务A,2000)}valjob2launch{asyncTask(任务B,1000)}job1.join()job2.join()// 使用 async 返回结果valresultasync{delay(500)计算结果}println(result.await())}说明Kotlin 通过协程库kotlinx.coroutines提供无栈协程使用launch、async等构建器语法简洁。10. 总结协程与线程如何协同协程并不是要取代线程而是与线程协同工作协程负责高并发、多任务调度逻辑流管理提供轻量级的任务切换。线程负责真正利用多核 CPU物理执行同时作为协程的载体。一种常见架构是启动与 CPU 核心数量相等的线程每个线程内运行一个事件循环和成千上万个协程。这样既能充分利用多核又能轻松支撑百万级并发连接。理解协程只需抓住四个字挂起、恢复。它是一种优雅而强大的并发抽象正越来越多地被现代编程语言和框架采纳。掌握协程将帮助你写出更高性能、更易维护的 I/O 密集型程序。本文基于协程的核心思想“主动挂起、恢复执行”展开从概念、原理、对比、实现到应用场景全方位介绍了协程。希望能帮助读者建立对协程的系统认识并在实际开发中合理运用。