Go语言并发机制初探

Go 语言相比Java等一个很大的优点就是能够方便地编写并发程序。Go 语言内置了 goroutine 机制,使用goroutine能够快速地开发并发程序, 更好的利用多核处理器资源。这篇文章学习 goroutine 的应用及其调度实现。golang

 

1、Go语言对并发的支持

使用goroutine编程

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

goroutine的用法以下:windows

//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核数。spa

配置并行执行比较适合适合于CPU密集型、并行度比较高的情景,若是是IO密集型使用多核的化会增长cpu切换带来的性能损失。

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

 

2、区别并行与并发

进程、线程与处理器

在现代操做系统中,线程是处理器调度和分配的基本单位,进程则做为资源拥有的基本单位。每一个进程是由私有的虚拟地址空间、代码、数据和其它各类系统资源组成。线程是进程内部的一个执行单元。 每个进程至少有一个主执行线程,它无需由用户去主动建立,是由系统自动建立的。 用户根据须要在应用程序中建立其它线程,多个线程并发地运行于同一个进程中。

并行与并发

并行与并发(Concurrency and Parallelism)是两个不一样的概念,理解它们对于理解多线程模型很是重要。

在描述程序的并发或者并行时,应该说明从进程或者线程的角度出发。

  • 并发:一个时间段内有不少的线程或进程在执行,但什么时候间点上都只有一个在执行,多个线程或进程争抢时间片轮流执行

  • 并行:一个时间段和时间点上都有多个线程或进程在执行

非并发的程序只有一个垂直的控制逻辑,在任什么时候刻,程序只会处在这个控制逻辑的某个位置,也就是顺序执行。若是一个程序在某一时刻被多个CPU流水线同时进行处理,那么咱们就说这个程序是以并行的形式在运行。

并行须要硬件支持,单核处理器只能是并发,多核处理器才能作到并行执行。

  • 并发是并行的必要条件,若是一个程序自己就不是并发的,也就是只有一个逻辑执行顺序,那么咱们不可能让其被并行处理。

  • 并发不是并行的充分条件,一个并发的程序,若是只被一个CPU进行处理(经过分时),那么它就不是并行的。

举一个例子,编写一个最简单的顺序结构程序输出"Hello World",它就是非并发的,若是在程序中增长多线程,每一个线程打印一个"Hello World",那么这个程序就是并发的。若是运行时只给这个程序分配单个CPU,这个并发程序还不是并行的,须要部署在多核处理器上,才能实现程序的并行。

 

3、几种不一样的多线程模型

用户线程与内核级线程

线程的实现能够分为两类:用户级线程(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,内核用户空间综合了前两种的优势。 

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

 

4、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中。 多核处理器的场景下,为了运行goroutines,每一个M系统线程会持有一个Processor。

在正常状况下,scheduler会按照上面的流程进行调度,可是线程会发生阻塞等状况,看一下goroutine对线程阻塞等的处理。

线程阻塞

当正在运行的goroutine阻塞的时候,例如进行系统调用,会再建立一个系统线程(M1),当前的M线程放弃了它的Processor,P转到新的线程中去运行。

runqueue执行完成

当其中一个Processor的runqueue为空,没有goroutine能够调度。它会从另一个上下文偷取一半的goroutine。

 

5、对并发实现的进一步思考

Go语言的并发机制还有不少值得探讨的,好比Go语言和Scala并发实现的不一样,Golang CSP 和Actor模型的对比等。

了解并发机制的这些实现,能够帮助咱们更好的进行并发程序的开发,实现性能的最优化。

关于三种多线程模型,能够关注一下Java语言的实现。

咱们知道Java经过JVM封装了底层操做系统的差别,而不一样的操做系统可能使用不一样的线程模型,例如Linux和windows可能使用了一对一模型,solaris和unix某些版本可能使用多对多模型。JVM规范里没有规定多线程模型的具体实现,1:1(内核线程)、N:1(用户态线程)、M:N(混合)模型的任何一种均可以。谈到Java语言的多线程模型,须要针对具体JVM实现,好比Oracle/Sun的HotSpot VM,默认使用1:1线程模型。

 

参考连接
相关文章
相关标签/搜索