其实从一开始了解到go的goroutine概念就应该想到,其实go应该就是在内核级线程的基础上做了一层逻辑上的虚拟线程(用户级线程)+ 线程调度系统,如此分析以后,goroutine也就不再那么神秘了。
并发≠并行
假如我们有一段CPU密集型任务,我们创建2000个gorountine是否真的可以将其性能提高2000倍,答案必然是不能,因为我们只是进行了2000次的并发(concurrency),而并没有真正做到并行(parallelism)。
并发其实所指的是我们的程序执行逻辑,传统单线程应用的程序逻辑是顺序执行的,在任何时刻,程序只能处理同一个逻辑,而并发指的是,我们同时执行多个独立的程序逻辑,若干个程序逻辑在执行时可以是同时进行的(但并不代表同时进行处理)。实际上,不论我们并发多少个程序逻辑,若我们仅仅将其运行在一个单核单线程的CPU上,都不能让你的程序在性能上有所提升,因为最终所有任务都排队等待CPU资源(时间片)。
而并行才能让我们的程序真正的同时处理多个任务,但并行并不是编程语言能够带我们的特性,他需要硬件支持。上面说到单核CPU所有资源都要等待同一个CPU的资源,那么其实我们只要将CPU增多就能真正的让我们实现并行。我们可以使用多核CPU或用多台服务器组成服务集群,均可实现真正的并行,能够并行处理的任务数量也就是我们的CPU数量。
引用Rob Pikie大神在PPT《Concurrency is not Parallelism》中的一段总结,大意就是并发不同于并行,但并发也许可以让程序实现并行化。
Concurrency vs. parallelism
Concurrency is about dealing with lots of things at once.
Parallelism is about doing lots of things at once.
Not the same, but related.
Concurrency is about structure, parallelism is about execution.
Concurrency provides a way to structure a solution to solve a problem that may (but not necessarily) be parallelizable.
CPU密集&I/O密集
那么是不是说我们在单核CPU的机器上使用编程语言所提供的多线程就没有意义了呢?
如果说我们的程序属于CPU密集型,使用并发编程,可能确实无法提升我们的程序性能,甚至可能因为大量计算资源花在了创建线程本身,导致程序性能进一步下降。
但不同的是,如果说我们的程序属于IO密集型,当你在进行程序压测的过程中可能发现CPU占用率很低,但性能却到了瓶颈,原因是程序将大量的时间花在了等待IO的过程中,如果我们可以在等待IO的时候继续执行其他的程序逻辑即可提高CPU利用率,从而提高我们的程序性能,这时并发编程的好处就出来了,例如Python因为GIL的存在实际上并不能实现真正的并行,但他的多线程依旧在IO密集型的程序中依旧有种很重要的意义。
Goroutine(Golang Coroutine)
上面说到了使用多核CPU实现并行处理,使应用在多核cpu实现并行处理的方案主要是多进程与多线程两种方式,多进程模型相对简单,但是有着资源开销大及进程间通信成本高的问题。多线程模型相对复杂,会有死锁,线程安全,模型复杂等问题,但却因为资源开销及易于管理等优点适用于对于性能要求较高的应用。
Golang采用的是多线程模型,更详细的说他是一个两级线程模型,但它对系统线程(内核级线程)进行了封装,暴露了一个轻量级的协程goroutine(用户级线程)供用户使用,而用户级线程到内核级线程的调度由golang的runtime负责,调度逻辑对外透明。
goroutine的优势在于上下文切换在完全用户态进行,无需像线程一样频繁在用户态与内核态之间切换,节约了资源消耗。
同时,启动一个gorountine非常简单,而且写法很cool~
go function()
仅仅需要在调用函数时在前面加上关键字go即可创建一个goroutine并创建其上下文对象。
G·P·M
G(Goroutine) :我们所说的协程,为用户级的轻量级线程,每个Goroutine对象中的sched保存着其上下文信息
M(Machine) :对内核级线程的封装,数量对应真实的CPU数(真正干活的对象)
P(Processor) :即为G和M的调度对象,用来调度G和M之间的关联关系,其数量可通过GOMAXPROCS()来设置,默认为核心数
每个Processor对象都拥有一个LRQ(Local Run Queue),未分配的Goroutine对象保存在GRQ(Global Run Queue )中,等待分配给某一个P的LRQ中,每个LRQ里面包含若干个用户创建的Goroutine对象,同时Processor作为桥梁对Machine和Goroutine进行了解耦,也就是说Goroutine如果想要使用Machine需要绑定一个Processor才行,上图中共有两个M和两个P也就是说我们可以同时并行处理两个goroutine。
这一篇主要讲解了一些并行与并发的区别Golang并发模型的优势,下一篇会详细说明GPM模型的调度策略。