线程和协程的区别了解
1.资源消耗
- 调度方式:线程由操作系统内核调度(抢占式),协程由程序自己控制调度(协作式)。
- 切换开销:线程切换涉及内核态与用户态的转换,开销大;协程只在用户态切换上下文,开销小。
- 状态保存:线程保存寄存器、栈、指令指针等完整上下文;协程只保存少量栈、程序计数器等上下文。
- 资源隔离: 各线程有自己的堆栈,数据隔离;同一进程内协程共享内存,需注意同步。
✅ 线程创建 & 销毁:
-
创建:需要申请内核资源(线程栈、PCB 等),大约消耗 几 MB 内存
-
销毁:回收内核资源,代价也不小
-
上下文切换:涉及用户态/内核态切换,保存寄存器、栈、内存页表,切换一次可能 耗时 1000ns+
✅ 协程创建 & 销毁:
-
创建:只需要申请一小段栈空间(一般几 KB),几乎不涉及内核资源
-
销毁:内存回收代价极小
-
上下文切换:只保存少量寄存器和栈,切换一次只需 几十纳秒(几乎不感知)。可以理解为用户态的调度器。
2.适用情况
👉 总结一句话:
-
线程是“重装战士”,适合 多核并行 + CPU 密集型任务
-
协程是“敏捷刺客”,适合 高并发 IO 密集型任务
3.内核态和用户态
🔍 线程为什么开销高?
-
用户态 ↔️ 内核态:涉及 系统调用(
syscall
),需要 切换模式,如read()
、write()
都要从用户态陷入内核态。涉及到操作系统调用。 -
内核栈切换:每个线程都有独立内核栈,切换需要重新加载。
-
缓存失效:切换线程可能导致 CPU 缓存失效,重建 TLB(地址翻译缓存)也增加延迟。
💥 开销对比:
-
线程切换(内核调度):大约 1000ns+
-
进程切换(内核调度 + 页表切换):大约 几微秒
-
协程切换(用户态调度):大约 几十纳秒
4.代码例子
chatgpt给出的切换线程和协程的耗时,在数量都是1w的情况下,切换协程的耗时居然比较高,数量如果达到10w,是线程切换的耗时比较高。
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
const (
numGoroutines = 100000
numThreads = 100000
)
// 模拟一个消耗时间的任务
func doTask(id int, ch chan int, wg *sync.WaitGroup) {
defer wg.Done()
// 模拟一些计算任务
for i := 1; i < 1000; i++ {
_ = (i * i) % id
}
ch <- id
}
// 记录线程切换时间
func measureThreadSwitching() {
start := time.Now()
var wg sync.WaitGroup
for i := 1; i < numThreads; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// 模拟一个计算任务
_ = id * id
}(i)
}
wg.Wait()
elapsed := time.Since(start)
fmt.Printf("Thread switching took: %v\n", elapsed)
}
// 记录协程切换时间
func measureGoroutineSwitching() {
start := time.Now()
var wg sync.WaitGroup
ch := make(chan int, numGoroutines)
for i := 1; i < numGoroutines; i++ {
wg.Add(1)
go doTask(i, ch, &wg)
}
wg.Wait()
elapsed := time.Since(start)
fmt.Printf("Goroutine switching took: %v\n", elapsed)
}
func main() {
// 设置 Go 运行时使用多个 CPU 核心
runtime.GOMAXPROCS(runtime.NumCPU())
// 测量线程切换的时间
fmt.Println("Measuring thread switching...")
measureThreadSwitching()
// 测量协程切换的时间
fmt.Println("Measuring goroutine switching...")
measureGoroutineSwitching()
}