为什么需要协程?

  • 高并发 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 文件名: asyncio_example.py
import asyncio
import time

async def io_task(id: int) -> int:
# 模拟一个异步IO:不会阻塞事件循环
print(f"task {id} start")
await asyncio.sleep(1) # suspend 点,交出执行权
print(f"task {id} done")
return id * 2

async def main():
t0 = time.perf_counter()
# 创建并启动多个协程任务
tasks = [asyncio.create_task(io_task(i)) for i in range(10)]
# 等待所有任务完成并收集返回值
results = await asyncio.gather(*tasks)
print("results:", results)
print("elapsed:", time.perf_counter() - t0)

if __name__ == "__main__":
asyncio.run(main())

变量/关键点说明

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 文件名: goroutine_example.go
package main

import (
"fmt"
"sync"
"time"
)

func worker(id int, wg *sync.WaitGroup, ch chan<- int) {
defer wg.Done() // 标记此 goroutine 完成
fmt.Printf("worker %d start\n", id)
time.Sleep(1 * time.Second) // 模拟阻塞的 I/O 或工作(Go 的调度器会抢占)
fmt.Printf("worker %d done\n", id)
ch <- id * 2
}

func main() {
var wg sync.WaitGroup
n := 10
ch := make(chan int, n) // 带缓冲的 channel 保存结果
wg.Add(n)
for i := 0; i < n; i++ {
go worker(i, &wg, ch) // 启动 goroutine(非常轻量)
}

// 等待所有 goroutine 完成后关闭 channel
go func() {
wg.Wait()
close(ch)
}()

// 收集结果
results := []int{}
for r := range ch {
results = append(results, r)
}
fmt.Println("results:", results)
}

变量/关键点说明

  • go worker(...):启动一个 goroutine(类似协程,Go 运行时管理调度)。
  • sync.WaitGroup:等待一组 goroutine 完成。
  • chan:用于 goroutine 间通信(线程安全)。
  • time.Sleep:在 Go 中是可以的,因为 Go 运行时会进行抢占调度并把 goroutine 挪到别的 M(内核线程)上执行其它 goroutine,从而能利用多核并行。

注意

  • goroutine 数量非常大时也要限制并发(例如使用带缓冲的 channel 或者信号量模式),以免资源仍被耗尽(例如打开大量文件句柄、网络连接等)。

Kotlin(协程)示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 文件名: KotlinCoroutineExample.kt
import kotlinx.coroutines.*

suspend fun ioTask(id: Int): Int {
println("task $id start on ${Thread.currentThread().name}")
delay(1000L) // 非阻塞挂起点(等同于 asyncio.sleep)
println("task $id done")
return id * 2
}

fun main() = runBlocking {
val t0 = System.currentTimeMillis()
// structured concurrency:在 runBlocking/ coroutineScope 内启动的子协程会被父作用域管理
val deferred = (0 until 10).map { id ->
async(Dispatchers.Default) { // Dispatchers.Default 可并行使用多核
ioTask(id)
}
}

val results = deferred.awaitAll() // 等待所有结果
println("results: $results")
println("elapsed: ${System.currentTimeMillis() - t0} ms")

// 演示取消
val job = launch {
repeat(100) { i ->
println("working $i")
delay(100L)
}
}
delay(350L)
job.cancelAndJoin() // 取消并等待结束(协程遇到挂起点时会响应取消)
println("job cancelled")
}

变量/关键点说明

  • 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(如 withTimeoutasyncio.wait_for)。
  • 调试/栈追踪:协程栈有时不如线程直观,利用语言提供的调试工具(traceback,profilers)和日志。
  • 区分并发与并行:如果是 CPU 密集型任务,考虑使用线程池或分配到多核(或把重计算交给 native 代码/分布式系统)。

总结(精要)

  • 协程用于高并发、I/O 密集场景,提供轻量级的并发,比线程更低开销、代码更易读。
  • 协程与线程在调度、阻塞行为、资源开销上有本质不同;是否并行取决于运行时和调度器。
  • 使用协程时注意别阻塞事件循环/调度线程、使用结构化并发、控制并发量与正确处理取消/超时。

如果你想,我可以:

  • 把上面任一语言的示例扩展为更贴近你实际场景(比如并发 HTTP 请求的真实示例、带超时/重试/限速的实现),或者
  • 针对你常用语言(比如你更想看 Java/Kotlin/Go/Python 中的哪一种)给出更详细的实战模板(含错误处理、超时、限流、metrics 等)。

你要哪个方向我就直接给出对应的实战实现(含注释和关键点讲解)。