并行与并发
并发:同一时段内执行多个任务
并行:同一时刻执行多个任务
Go语言并发通过goroutine
实现。goroutine
类似于线程,属于用户态的线程,可以根据需要创建成千上万个goroutine
并发工作。goroutine
由Go语言的运行时(runtime)调度完成,而线程由操作系统调度完成。
Go语言提供channel
在多个goroutine
进行通信。goroutine
和channel
是Go语言秉承的CSP(Communicating Sequential Process)并发模式的重要实现基础。
goroutine与GMP模型
可增长的栈
内核态线程一半都有固定的栈内存(通常有2MB),一个goroutine
的栈在其声明周期开始时只有很小的栈(典型情况下为2KB),goroutine
的栈是不固定的,其可以按需增大和缩小。goroutine
的栈大小可以达到1GB。在GO语言中一次创建十万左右的goroutine
也是可以的。
goroutine调度模型GMP
GMP
是Go语言运行时层面的实现,是Go语言自己实现的一套调度系统。区别于操作系统调度OS线程。
G
:单个goroutine运行时,里面除了存放本goroutine信息外,还存有于其所在P的绑定的的信息。M(Machine)
:Go运行时(runtime)对操作系统内核线程的虚拟,M与内核线程一般是一一对应的关系,一个goroutine最终都要放到M上执行。P
:管理一组goroutine队列,P里面会存储当前goroutine运行时的上下文环境(函数指针,堆栈地址以及地址边界),P会对自己管理的giroutine队列最一些调度,当自己的队列消费完就去全局队列中取,如果全局队列也消费完毕就去其P他队列中抢任务。
P与M一般也是一一对应的。他们的关系是:P管理着一组G挂载在M上运行。当一个G长久阻塞在M上时,runtime会新建一个M,阻塞G所在的P会把其他的G挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时,回收旧的M。
P的个数是通过runtime.GOMAXPROCS
设定(最大256),GO1.5版本后默认为物理线程数。在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。
单从线程调度讲,Go语言相比其他语言的优势在于OS线程是由OS内核调度,goroutine
则是由Go运行时runtime自己的调度器调度,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。一大特点是goroutine的调度是在用户态下完成的,不涉及内核态和用户态之间的频繁切换,包括内存的分配与释放都是在用户态维持着一块大的内存池,不直接调用系统的malloc函数,成本调用比调度OS线程低很多。另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上,再加上本身的goroutine的超轻量,以上种种保证了Go调度方面的性能。
GOMAXPROCS
Go运行时的调度器使用GOMAXPROCS
参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。(GOMAXPROCS是m:n调度中的n)
GO语言可以通过使用runtime.GOMAXPROCS()
设置当前程序并发时占用的逻辑核心数.
GO1.5版本之前,默认使用单核心执行.Go1.5版本中之后,默认使用全部的CPU逻辑核心数.
Go语言中的操作系统线程和goroutine的关系:
- 一个操作系统线程对应用户态多个goroutine
- go程序可以同时使用多个操作系统线程
- goroutine和OS线程时多对多的关心,即m:n