golang 协程和线程的区别
协程
协程,英文名Coroutine。但在 Go 语言中,协程的英文名是:gorutine。它常常被用于进行多任务,即并发作业。没错,就是多线程作业的那个作业。
虽然在 Go 中,我们不用直接编写线程之类的代码来进行并发,但是 Go 的协程却依赖于线程来进行。
下面我们来看看它们的区别。
协程的特点
这里先直接列出线程的特点,然后从例子中进行解析。
1. 多个协程可由一个或多个线程管理,协程的调度发生在其所在的线程中。
2. 可以被调度,调度策略由应用层代码定义,即可被高度自定义实现。
3. 执行效率高。
4. 占用内存少。
关于 1 和 2
runtime.GOMAXPROCS(1)
time.Sleep(time.Second)
添加这两个代码的作用, 分别是 :
指定在几个线程内执行
等待时间
如果不设置 runtime.GOMAXPROCS(1),那么程序将会根据操作系统的 CPU 核数而启动对应数量的 P,导致多个 M,即线程的启动。那么我们程序中的协程,就会被分配到不同的线程里面去了。设置数量 1,使得它们都被分配到了同一个线程里面,存于线程的协程队列里面,等待被执行或调度。
关于 3 和 4
因为协程的调度切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。调度发生在应用态而非内核态。
内存的花销,使用其所在的线程的内存,意味着线程的内存可以供多个协程使用。
其次协程的调度不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,所以执行效率比多线程高很多。
和线程的整体对比
| 比较的点 | 线程 | 协程 |
|---|---|---|
| 数据存储 | 内核态的内存空间 | 一般是线程提供的用户态内存空间 |
| 切换操作 | 操作最终在内核层完成,应用层需要调用内核层提供的 syscall 底层函数 | 应用层使用代码进行简单的现场保存和恢复即可 |
| 任务调度 | 由内核实现,抢占方式,依赖各种锁 | 由用户态的实现的具体调度器进行。例如 go 协程的调度器 |
| 语音支持程度 | 绝大部分编程语言 | 部分语言:Lua,Go,Python ... |
| 实现规范 | 按照现代操作系统规范实现 | 无统一规范。在应用层由开发者实现,高度自定义,比如只支持单线程的线程。不同的调度策略,等等 |
--------------------------------------------------------------------------------------------------------------------------------------------------------
一般来说,协程就像轻量级的线程。
线程一般有固定的栈,有一个固定的大小。而goroutines为了避免资源浪费(亦或是资源缺乏),采用动态扩张收缩的策略:初始量为2k,最大可以扩张到1G。
每个线程都有一个id,在线程创建的时候就会被返回,所以我们可以通过线程的id来操纵线程。但是在golang中没有这个概念,因此我们在编码之初就要考虑协程的创建和释放问题。
线程和 goroutine 切换调度开销方面
线程/goroutine 切换开销方面,goroutine 远比线程小
线程:涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP...等寄存器的刷新等。
因为协程在用户态由协程调度器完成,不需要陷入内核,这代价就小了。
所以goroutine:只有三个寄存器的值修改 - PC / SP / DX.