Golang:线程 和 协程 的区别
Golang:线程 和 协程 的区别
目录
- 前言
- 协程
- 协程的特点
第 1
和第 2
点- 特点中的第 3 和 第 4 点
- 和线程的整体对比
协程
协程,英文名Coroutine
。但在 Go 语言中,协程的英文名是:gorutine
。它常常被用于进行多任务
,即并发作业
。没错,就是多线程
作业的那个作业。
虽然在 Go 中,我们不用直接编写线程之类的代码来进行并发,但是 Go 的协程却依赖于线程
来进行。
协程的特点
这里先直接列出线程的特点,然后从例子中进行解析。
- 多个协程可由一个或多个线程管理,
协程的调度
发生在其所在的线程中。 - 可以被调度,调度策略由应用层代码定义,即可被高度自定义实现。
- 执行效率高。
- 占用内存少。
上面第 1
和 第 2
点
我们来看一个例子:
func TestGorutine(t *testing.T) {
runtime.GOMAXPROCS(1) // 指定最大 P 为 1,从而管理协程最多的线程为 1 个
wg := sync.WaitGroup{} // 控制等待所有协程都执行完再退出程序
wg.Add(2)
// 运行一个协程
go func() {
fmt.Println(1)
fmt.Println(2)
fmt.Println(3)
wg.Done()
}()
// 运行第二个协程
go func() {
fmt.Println(65)
fmt.Println(66)
// 设置个睡眠,让该协程执行超时而被挂起,引起超时调度
time.Sleep(time.Second)
fmt.Println(67)
wg.Done()
}()
wg.Wait()
}
上面的代码片段跑了两个协程,运行后,观察输出的顺序是交错
的。可能是:
65
66
1
2
3
67
意味着在执行协程A的过程中,可以随时中断
,去执协程行B,协程B也可能在执行过程中中断再去执行协程A。
看起来协程A 和 协程B 的运行像是线程的切换,但是请注意,这里的 A 和 B 都运行在同一个线程里面。它们的调度不是线程的切换,而是纯应用态的协程调度
。
关于上述代码中,为什么要指定下面两行代码?
runtime.GOMAXPROCS(1)
time.Sleep(time.Second)
如果不设置 runtime.GOMAXPROCS(1)
,那么程序将会根据操作系统的 CPU 核数而启动对应数量的 P,导致多个 M,即线程的启动。那么我们程序中的协程,就会被分配到不同的线程
里面去了。为了演示,故设置数量 1,使得它们都被分配到了同一个线程里面,存于线程的协程队列里面,等待被执行或调度。
协程特点中的第 3 和 第 4 点。
- 执行效率高。
- 占用内存少。
因为协程的调度切换不是线程切换
,而是由程序自身控制,因此,没有线程切换的开销
,和多线程比,线程数量越多,协程的性能优势就越明显。调度发生在应用态而非内核态。
内存的花销,使用其所在的线程的内存,意味着线程的内存可以供多个协程使用。
其次协程的调度不需要多线程的锁机制
,因为只有一个线程,也不存在同时写变量冲突
,所以执行效率比多线程高很多。
线程和协程的整体对比
比较的点 | 线程 | 协程 |
---|---|---|
数据存储 | 内核态的内存空间 | 一般是线程提供的用户态内存空间 |
切换操作 | 操作最终在内核层完成,应用层需要调用内核层提供的 syscall 底层函数 | 应用层使用代码进行简单的现场保存和恢复即可 |
任务调度 | 由内核实现,抢占方式,依赖各种锁 | 由用户态的实现的具体调度器进行。例如 go 协程的调度器 |
语音支持程度 | 绝大部分编程语言 | 部分语言:Lua,Go,Python ... |
实现规范 | 按照现代操作系统规范实现 | 无统一规范。在应用层由开发者实现,高度自定义,比如只支持单线程的线程。不同的调度策略,等等 |