Go语言并发机制

 

Go语言中的并发

使用goroutine编程

使用 go 关键字用来建立 goroutine 。将go声明放到一个需调用的函数以前,在相同地址空间调用运行这个函数,这样该函数执行时便会做为一个独立的并发线程。这种线程在Go语言中称做goroutine。java

goroutine的用法以下:golang

//go 关键字放在方法调用前新建一个 goroutine 并执行方法体
go GetThingDone(param1, param2);

//新建一个匿名方法并执行
go func(param1, param2) {
}(val1, val2)

//直接新建一个 goroutine 并在 goroutine 中执行代码块
go {
    //do someting...
}

由于 goroutine 在多核 cpu 环境下是并行的。若是代码块在多个 goroutine 中执行,咱们就实现了代码并行。编程

若是咱们须要了解程序的执行状况,怎么拿到并行的结果呢?须要配合使用channel进行。数组

使用Channel控制并发

Channels用来同步并发执行的函数并提供它们某种传值交流的机制。安全

经过channel传递的元素类型、容器(或缓冲区)和传递的方向由“<-”操做符指定。数据结构

可使用内置函数 make分配一个channel:多线程

i := make(chan int)       // by default the capacity is 0
s := make(chan string, 3) // non-zero capacity

r := make(<-chan bool)          // can only read from
w := make(chan<- []os.FileInfo) // can only write to

配置runtime.GOMAXPROCS

使用下面的代码能够显式的设置是否使用多核来执行并发任务:并发

runtime.GOMAXPROCS() 

GOMAXPROCS的数目根据任务量分配就能够,可是不要大于cpu核数。 
配置并行执行比较适合适合于CPU密集型、并行度比较高的情景,若是是IO密集型使用多核的化会增长cpu切换带来的性能损失。函数

了解了Go语言的并发机制,接下来看一下goroutine 机制的具体实现。性能

 

 

Go的CSP并发模型

Go实现了两种并发形式。第一种是你们广泛认知的:多线程共享内存。其实就是Java或者C++等语言中的多线程开发。另一种是Go语言特有的,也是Go语言推荐的:CSP(communicating sequential processes)并发模型。

CSP并发模型是在1970年左右提出的概念,属于比较新的概念,不一样于传统的多线程经过共享内存来通讯,CSP讲究的是“以通讯的方式来共享内存”。

请记住下面这句话:
Do not communicate by sharing memory; instead, share memory by communicating.
“不要以共享内存的方式来通讯,相反,要经过通讯来共享内存。”

普通的线程并发模型,就是像Java、C++、或者Python,他们线程间通讯都是经过共享内存的方式来进行的。很是典型的方式就是,在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,经过锁来访问,所以,在不少时候,衍生出一种方便操做的数据结构,叫作“线程安全的数据结构”。例如Java提供的包”java.util.concurrent”中的数据结构。Go中也实现了传统的线程并发模型。

Go的CSP并发模型,是经过goroutinechannel来实现的。

  • goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“相似,能够理解为”线程“。
  • channel是Go语言中各个并发结构体(goroutine)以前的通讯机制。 通俗的讲,就是各个goroutine之间通讯的”管道“,有点相似于Linux中的管道。

生成一个goroutine的方式很是的简单:Go一下,就生成了。

go f();

 

通讯机制channel也很方便,传数据用channel <- data,取数据用<-channel

在通讯过程当中,传数据channel <- data和取数据<-channel必然会成对出现,由于这边传,那边取,两个goroutine之间才会实现通讯。

并且无论传仍是取,必阻塞,直到另外的goroutine传或者取为止。

有两个goroutine,其中一个发起了向channel中发起了传值操做。(goroutine为矩形,channel为箭头)

左边的goroutine开始阻塞,等待有人接收。

这时候,右边的goroutine发起了接收操做。

 

右边的goroutine也开始阻塞,等待别人传送。

这时候,两边goroutine都发现了对方,因而两个goroutine开始一传,一收。

 

这即是Golang CSP并发模型最基本的形式。

 

 

 

几种不一样的多线程模型

用户线程与内核级线程

线程的实现能够分为两类:用户级线程(User-LevelThread, ULT)和内核级线程(Kemel-LevelThread, KLT)。用户线程由用户代码支持,内核线程由操做系统内核支持。

多线程模型

多线程模型即用户级线程和内核级线程的不一样链接方式。

(1)多对一模型(M : 1)

将多个用户级线程映射到一个内核级线程,线程管理在用户空间完成。
此模式中,用户级线程对操做系统不可见(即透明)。

 

优势:
这种模型的好处是线程上下文切换都发生在用户空间,避免的模态切换(mode switch),从而对于性能有积极的影响。
缺点:全部的线程基于一个内核调度实体即内核线程,这意味着只有一个处理器能够被利用,在多处理环境下这是不可以被接受的,本质上,用户线程只解决了并发问题,可是没有解决并行问题。

若是线程由于 I/O 操做陷入了内核态,内核态线程阻塞等待 I/O 数据,则全部的线程都将会被阻塞,用户空间也可使用非阻塞而 I/O,可是仍是有性能及复杂度问题。

 

(2) 一对一模型(1:1)

将每一个用户级线程映射到一个内核级线程。

 

每一个线程由内核调度器独立的调度,因此若是一个线程阻塞则不影响其余的线程。
优势:在多核处理器的硬件的支持下,内核空间线程模型支持了真正的并行,当一个线程被阻塞后,容许另外一个线程继续执行,因此并发能力较强。

缺点:每建立一个用户级线程都须要建立一个内核级线程与其对应,这样建立线程的开销比较大,会影响到应用程序的性能。

 

(3)多对多模型(M : N)

内核线程和用户线程的数量比为 M : N,内核用户空间综合了前两种的优势。

这种模型须要内核线程调度器和用户空间线程调度器相互操做,本质上是多个线程被绑定到了多个内核线程上,这使得大部分的线程上下文切换都发生在用户空间,而多个内核线程又能够充分利用处理器资源。

 

 

 

goroutine机制的调度实现

goroutine机制实现了M : N的线程模型,goroutine机制是协程(coroutine)的一种实现,golang内置的调度器,可让多核CPU中每一个CPU执行一个协程。

理解goroutine机制的原理,关键是理解Go语言scheduler的实现。

调度器是如何工做的

Go语言中支撑整个scheduler实现的主要有4个重要结构,分别是M、G、P、Sched,
前三个定义在runtime.h中,Sched定义在proc.c中。

  • Sched结构就是调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。
  • M结构是Machine,系统线程,它由操做系统管理的,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等很是多的信息。
  • P结构是Processor,处理器,它的主要用途就是用来执行goroutine的,它维护了一个goroutine队列,即runqueue。Processor是让咱们从N:1调度到M:N调度的重要部分。
  • G是goroutine实现的核心结构,它包含了栈,指令指针,以及其余对调度goroutine很重要的信息,例如其阻塞的channel。

Processor的数量是在启动时被设置为环境变量GOMAXPROCS的值,或者经过运行时调用函数GOMAXPROCS()进行设置。Processor数量固定意味着任意时刻只有GOMAXPROCS个线程在运行go代码。

参考这篇传播很广的博客:http://morsmachine.dk/go-scheduler
咱们分别用三角形,矩形和圆形表示Machine Processor和Goroutine。

在单核处理器的场景下,全部goroutine运行在同一个M系统线程中,每个M系统线程维护一个Processor,任什么时候刻,一个Processor中只有一个goroutine,其余goroutine在runqueue中等待。一个goroutine运行完本身的时间片后,让出上下文,回到runqueue中。

以上这个图讲的是两个线程(内核线程)的状况。一个M会对应一个内核线程,一个M也会链接一个上下文P,一个上下文P至关于一个“处理器”,一个上下文链接一个或者多个Goroutine。P(Processor)的数量是在启动时被设置为环境变量GOMAXPROCS的值,或者经过运行时调用函数runtime.GOMAXPROCS()进行设置。Processor数量固定意味着任意时刻只有固定数量的线程在运行go代码。Goroutine中就是咱们要执行并发的代码。图中P正在执行的Goroutine为蓝色的;处于待执行状态的Goroutine为灰色的,灰色的Goroutine造成了一个队列runqueues

三者关系的宏观的图为:

 

抛弃P(Processor)

你可能会想,为何必定须要一个上下文,咱们能不能直接除去上下文,让Goroutinerunqueues挂到M上呢?答案是不行,须要上下文的目的,是让咱们能够直接放开其余线程,当遇到内核线程阻塞的时候。

一个很简单的例子就是系统调用sysall,一个线程确定不能同时执行代码和系统调用被阻塞,这个时候,此线程M须要放弃当前的上下文环境P,以即可以让其余的Goroutine被调度执行。

 

如上图左图所示,M0中的G0执行了syscall,而后就建立了一个M1(也有可能自己就存在,没建立),(转向右图)而后M0丢弃了P,等待syscall的返回值,M1接受了P,将·继续执行Goroutine队列中的其余Goroutine

当系统调用syscall结束后,M0会“偷”一个上下文,若是不成功,M0就把它的Gouroutine G0放到一个全局的runqueue中,而后本身放到线程池或者转入休眠状态。全局runqueue是各个P在运行完本身的本地的Goroutine runqueue后用来拉取新goroutine的地方。P也会周期性的检查这个全局runqueue上的goroutine,不然,全局runqueue上的goroutines可能得不到执行而饿死。

均衡的分配工做

按照以上的说法,上下文P会按期的检查全局的goroutine 队列中的goroutine,以便本身在消费掉自身Goroutine队列的时候有事可作。假如全局goroutine队列中的goroutine也没了呢?就从其余运行的中的P的runqueue里偷。

每一个P中的Goroutine不一样致使他们运行的效率和时间也不一样,在一个有不少P和M的环境中,不能让一个P跑完自身的Goroutine就没事可作了,由于或许其余的P有很长的goroutine队列要跑,得须要均衡。
该如何解决呢?

Go的作法倒也直接,从其余P中偷一半!

 

 

 

参考:

https://yq.aliyun.com/articles/72365

http://morsmachine.dk/go-scheduler

https://studygolang.com/articles/11825

相关文章
相关标签/搜索