title: Goroutine(协程)理解
tags: Go,Goroutine
并发概念
Go语言相对于其他语言的最大一个特色就是支持高并发编程模式。Goroutine(协程)是Go中最基本的执行单元。事实上每一个Go程序至少有一个Goroutine:主Goroutine。当程序启动时,它会自动创建。
为了更好理解Goroutine,我们需要了解并发
和并行
的区别
- 并发:逻辑上具备同时处理多个任务的能力。
- 并行:物理上在同一时刻执行多个并发任务。
简单来说,并发
是在同一时间处理多件事情。并行
是在同一时间做多件事情。并发的目的在于把当个 CPU 的利用率使用到最高。并行则需要多核 CPU 的支持。
线程和协程
Go 语言在语言层面上支持了并发,goroutine是Go语言提供的一种用户态线程,有时我们也称之为协程。下面我们了解下协程和线程。
线程:时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程不用有自己都资源,但是能够共享进程拥有的全部资源
协程:所谓的协程,某种程度上也可以叫做轻量线程,它不由os而由应用程序创建和管理,因此使用开销较低。可以有效提高线程任务的并发性,避免高并发模式下线程的缺点。协程的最大优势在于其“轻量级”,可以轻松创建上百万个而不会导致系统资源衰竭。
在操作系统的OS Thread和编程语言的User Thread之间,实际上存在3种线程对应模型,也就是:1:1,1:N,M:N。
1:1 一个用户线程就只在一个内核线程上跑,这时可以利用多核,但是上下文切换很慢,切换效率很低。
N:1 多个(N)用户线程始终在一个内核线程上跑,context上下文切换很快,但是无法真正的利用多核。
M:N 多个goroutine在多个内核线程上跑,这个可以集齐上面两者的优势,既能快速切换上下文,也能利用多核的优势,而Go正是选择这种实现方式。
简单将 goroutine归纳为协程并不合适。运行时会创建多个线程来执行并发任务,且任务单元可被调度到其他线程并行执行。这更像是多线程和协程的综合体,能最大限度提升执行效率,发挥多核处理能力。
MPG模型
我们可以创建很多的goroutine,并且它们跑在同一个内核线程之上的时候,就需要一个调度器来维护这些goroutine,确保所有的goroutine都能使用cpu,并且是尽可能公平地使用cpu资源。
Go语言中调度器的主要有3个重要部分,分别是M、G、P。
- M (work thread) 代表了系统线程OS Thread,由操作系统管理。
- P (processor) 衔接M和G的调度上下文,它负责将等待执行的G与M对接。P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。
- G (goroutine) goroutine的实体,包括了调用栈,重要的调度信息,例如channel等。
使用goroutine
只须在函数调用前添加go关键字即可创建并发任务,关键字go并非执行并发操作,而是创建一个并发任务单元。新建任务被放置在系统队列中、等待调度器安排合适系统线程去获取执行权。
比如说,第一种方案,商场雇佣1000个导购员进行一对一服务,这种肯定是很高效的,但是造成资源大量浪费,而且管理困难。
第二种方案,商场雇佣20个导购员,对需要咨询的顾客进行处理,处理完,在处理剩下的顾客。
我们用代码来简单的演示一遍:
func loop() {
for i := 0; i <10 ; i++ {
fmt.Printf("%d ", i)
}
}
func main() {
go loop() // 启动一个goroutine
loop()
}
我们看下运行结果。
第一次:0 1 2 3 4 5 6 7 8 9 0 1
第二次:0 1 2 3 4 5 6 7 8 9 0
第三次:0 1 2 3 4 5 6 7 8 9 0 1 2
我们发现每次运行的结果都不同,这是因为goroutine当前流程不会阻塞,不会等待该任务启动,且运行时也不保证并发任务的执行次序。
总结
许多人可能认为goroutine比线程运行的更快,这是一个误解
goroutine并不会更快,它只是增加了更多的并发性。当一goroutine被阻塞(比如等待IO),golang的调度器会调度其它可以执行的goroutine运行。与线程相比,它有以下几个优点:
优点:
(1)内存消耗更少:
Goroutine所需要的内存通常只有2kb,而线程则需要1Mb
(2)创建与销毁的开销更小:
由于线程创建时需要向操作系统申请资源,并且在销毁时将资源归还,因此它的创建和销毁的开销比较大。相比之下,goroutine的创建和销毁是由go语言在运行时自己管理的,因此开销更低。
(3)切换开销更小
线程的调度方式是抢占式的,如果一个线程的执行时间超过了分配给它的时间片,就会被其它可执行的线程抢占;而goroutine的调度是协同式的,它不会直接地与操作系统内核打交道。缺点:
(1)协程调度机制无法实现公平调度。
有疑问加站长微信联系(非本文作者)