协程食用指南
为什么需要协程?
- 高并发 I/O 场景:传统线程每创建一个都要占用较大内存(线程栈、内核数据结构等),当连接数达到数万、数十万时,线程开销和上下文切换代价太高。协程更轻量,可以同时管理大量并发任务(例如大量短暂的网络请求、文件 I/O)。
- 简化异步编程:用回同步风格(直线式代码)写异步逻辑,代码可读性好,容易维护。协程通过
suspend/await
等机制把异步逻辑写成像同步的流程。 - 更低的调度开销:协程通常在用户态由语言/运行时调度(或混合调度),上下文切换更快,资源消耗更小。
- 结构化并发:现代协程库支持“结构化并发”(父协程生命周期管理子协程),便于取消、超时和错误传播。
协程 vs 线程 —— 主要区别(要点)
- 调度层面:线程由操作系统调度(内核线程),协程通常由语言运行时/库在用户态调度(有的实现也支持把协程调度到多个内核线程上,比如 Go 的调度器或 Java 虚拟线程)。
- 创建/销毁成本:协程成本远小于线程(内存占用少、创建快)。
- 上下文切换:协程上下文切换开销小;线程上下文切换需要内核介入,代价大。
- 阻塞行为:线程阻塞只影响该线程;协程如果在单线程事件循环中执行阻塞操作,会阻塞整个事件循环(必须使用非阻塞 API 或把阻塞操作移到线程池)。
- 并发 vs 并行:协程提供并发(在逻辑上同时进行多件事)。是否并行(同时使用多核)取决于运行时(例如 Go 可以并行,Python asyncio 默认单线程不并行 CPU-bound)。
- 调度方式:有的协程实现是协作式(只有在明确的“挂起点”才调度)(如 Kotlin、Python asyncio),有的实现带有抢占特性(如 Go 的运行时会抢占执行较长时间的 goroutine)。
什么时候用协程?
- 大量短连接或大量 I/O 操作(网络爬虫、高并发服务器、聊天/长连接服务、爬虫、并行文件 I/O)。
- 需要用异步风格组织复杂异步流程(易读、可取消、超时)。
- 不适合用于大量 CPU-bound 工作(除非运行时能把协程分配到多核上,或把 CPU-bound 工作移到线程池/工作线程)。
如何使用协程(语言示例:Python、Go、Kotlin)
下面给出三种常见语言的示例,展示基本用法与注意点。每段代码都是可运行的最小示例(注释为中文,解释变量与流程)。
Python(asyncio)示例
1 | # 文件名: asyncio_example.py |
变量/关键点说明
async def
:定义协程函数(需要await
)。await asyncio.sleep(1)
:挂起点,协程在此让出控制权,事件循环可以运行其他协程。asyncio.create_task(...)
:把协程包装成 Task 并安排立即执行(并行调度)。asyncio.gather(...)
:并行等待多个 Task 的完成。- 注意:不要在协程里使用
time.sleep()
(会阻塞整个事件循环)。若要执行阻塞的 CPU/IO 操作,使用asyncio.to_thread()
或loop.run_in_executor()
把它移到线程池中。
常用技巧
- 控制并发数:
Semaphore
限制同时运行的任务数。 - 超时/取消:
asyncio.wait_for(task, timeout)
或task.cancel()
。
Go(goroutine + channel)示例
1 | // 文件名: goroutine_example.go |
变量/关键点说明
go worker(...)
:启动一个 goroutine(类似协程,Go 运行时管理调度)。sync.WaitGroup
:等待一组 goroutine 完成。chan
:用于 goroutine 间通信(线程安全)。time.Sleep
:在 Go 中是可以的,因为 Go 运行时会进行抢占调度并把 goroutine 挪到别的 M(内核线程)上执行其它 goroutine,从而能利用多核并行。
注意
- goroutine 数量非常大时也要限制并发(例如使用带缓冲的 channel 或者信号量模式),以免资源仍被耗尽(例如打开大量文件句柄、网络连接等)。
Kotlin(协程)示例
1 | // 文件名: KotlinCoroutineExample.kt |
变量/关键点说明
suspend fun
:定义可挂起函数(协程体内的挂起点)。delay(...)
:协程挂起,不阻塞线程。runBlocking{}
:在主线程创建协程作用域并阻塞直到内部完成(常用于main
或测试)。async
/awaitAll()
:并发启动并收集返回值;launch
不返回值,仅返回Job
。Dispatchers.Default
:指定调度器,可利用多核并行(因此 Kotlin 协程既可并发又能并行,取决于调度器)。
注意
- 协程是协作式挂起:只有遇到挂起点(
delay
,withContext
,suspend
的实现等)才会交出执行权。 - 使用
withContext(Dispatchers.IO)
执行阻塞 I/O。
常见坑与实践建议(要点)
- 不要在协程中做阻塞调用(如
time.sleep()
、同步文件/网络 I/O),会阻塞事件循环/线程。若必须,移到线程池/调度器。 - 限制并发数(例如很多并发请求时使用 Semaphore/限流),避免打满文件描述符或外部资源。
- 优先使用结构化并发(父作用域管理子协程),便于取消与错误传播。
- 处理取消与超时:及时检查取消、在合适位置释放资源(
try { ... } finally { }
),使用超时 API(如withTimeout
、asyncio.wait_for
)。 - 调试/栈追踪:协程栈有时不如线程直观,利用语言提供的调试工具(traceback,profilers)和日志。
- 区分并发与并行:如果是 CPU 密集型任务,考虑使用线程池或分配到多核(或把重计算交给 native 代码/分布式系统)。
总结(精要)
- 协程用于高并发、I/O 密集场景,提供轻量级的并发,比线程更低开销、代码更易读。
- 协程与线程在调度、阻塞行为、资源开销上有本质不同;是否并行取决于运行时和调度器。
- 使用协程时注意别阻塞事件循环/调度线程、使用结构化并发、控制并发量与正确处理取消/超时。
如果你想,我可以:
- 把上面任一语言的示例扩展为更贴近你实际场景(比如并发 HTTP 请求的真实示例、带超时/重试/限速的实现),或者
- 针对你常用语言(比如你更想看 Java/Kotlin/Go/Python 中的哪一种)给出更详细的实战模板(含错误处理、超时、限流、metrics 等)。
你要哪个方向我就直接给出对应的实战实现(含注释和关键点讲解)。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Doraemon's Blog!
评论