Go语言最大的特点就是从语言层面支持并发(Goroutine),Goroutine是Go中最基本的执行单元。事实上每个Go程序至少有一个Goroutine:主Goroutine。当程序启动时,它会自动建立。java
为了更好理解Goroutine,现讲一下线程和协程的概念git
线程(Thread):有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程本身不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的所有资源。程序员
线程拥有本身独立的栈和共享的堆,共享堆,不共享栈,线程的切换通常也由操做系统调度。github
协程(coroutine):又称微线程与子例程(或者称为函数)同样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为通常和灵活,但在实践中使用没有子例程那样普遍。golang
和线程相似,共享堆,不共享栈,协程的切换通常由程序员在代码中显式控制。它避免了上下文切换的额外耗费,兼顾了多线程的优势,简化了高并发程序的复杂。数组
Goroutine和其余语言的协程(coroutine)在使用方式上相似,但从字面意义上来看不一样(一个是Goroutine,一个是coroutine),再就是协程是一种协做任务控制机制,在最简单的意义上,协程不是并发的,而Goroutine支持并发的。所以Goroutine能够理解为一种Go语言的协程。同时它能够运行在一个或多个线程上。缓存
先给个简单实例安全
func loop() { for i := 0; i < ; i++ { fmt.Printf("%d ", i) } } func main() { go loop() // 启动一个goroutine loop() }
1、Go并发模型服务器
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并发模型,是经过goroutine
和channel
来实现的。
goroutine
是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“相似,能够理解为”线程“。channel
是Go语言中各个并发结构体(goroutine
)以前的通讯机制。 通俗的讲,就是各个goroutine
之间通讯的”管道“,有点相似于Linux中的管道。生成一个goroutine
的方式很是的简单:Go一下,就生成了。
go f();
通讯机制channel
也很方便,传数据用channel <- data
,取数据用<-channel
。
在通讯过程当中,传数据channel <- data
和取数据<-channel
必然会成对出现,由于这边传,那边取,两个goroutine
之间才会实现通讯。
并且无论传仍是取,必阻塞,直到另外的goroutine
传或者取为止。
示例以下:
package main import "fmt" func main() { messages := make(chan string) go func() { messages <- "ping" }() msg := <-messages fmt.Println(msg) }
注意 main()自己也是运行了一个goroutine。
messages:= make(chan int) 这样就声明了一个阻塞式的无缓冲的通道
chan 是关键字 表明我要建立一个通道
咱们先从线程讲起,不管语言层面何种并发模型,到了操做系统层面,必定是以线程的形态存在的。而操做系统根据资源访问权限的不一样,体系架构可分为用户空间和内核空间;内核空间主要操做访问CPU资源、I/O资源、内存资源等硬件资源,为上层应用程序提供最基本的基础资源,用户空间呢就是上层应用程序的固定活动空间,用户空间不能够直接访问资源,必须经过“系统调用”、“库函数”或“Shell脚本”来调用内核空间提供的资源。
咱们如今的计算机语言,能够狭义的认为是一种“软件”,它们中所谓的“线程”,每每是用户态的线程,和操做系统自己内核态的线程(简称KSE),仍是有区别的。
线程模型的实现,能够分为如下几种方式:
如图所示,多个用户态的线程对应着一个内核线程,程序线程的建立、终止、切换或者同步等线程工做必须自身来完成。它能够作快速的上下文切换。缺点是不能有效利用多核CPU。
这种模型直接调用操做系统的内核线程,全部线程的建立、终止、切换、同步等操做,都由内核来完成。一个用户态的线程对应一个系统线程,它能够利用多核机制,但上下文切换须要消耗额外的资源。C++就是这种。
这种模型是介于用户级线程模型和内核级线程模型之间的一种线程模型。这种模型的实现很是复杂,和内核级线程模型相似,一个进程中能够对应多个内核级线程,可是进程中的线程不和内核线程一一对应;这种线程模型会先建立多个内核级线程,而后用自身的用户级线程去对应建立的多个内核级线程,自身的用户级线程须要自己程序去调度,内核级的线程交给操做系统内核去调度。
M个用户线程对应N个系统线程,缺点增长了调度器的实现难度。
Go语言的线程模型就是一种特殊的两级线程模型(GPM调度模型)。
M
指的是Machine
,一个M
直接关联了一个内核线程。由操做系统管理。P
指的是”processor”,表明了M
所需的上下文环境,也是处理用户级代码逻辑的处理器。它负责衔接M和G的调度上下文,将等待执行的G与M对接。G
指的是Goroutine
,其实本质上也是一种轻量级的线程。包括了调用栈,重要的调度信息,例如channel等。
P的数量由环境变量中的GOMAXPROCS
决定,一般来讲它是和核心数对应,例如在4Core的服务器上回启动4个线程。G会有不少个,每一个P会将Goroutine从一个就绪的队列中作Pop操做,为了减少锁的竞争,一般状况下每一个P会负责一个队列。
三者关系以下图所示:
以上这个图讲的是两个线程(内核线程)的状况。一个M会对应一个内核线程,一个M也会链接一个上下文P,一个上下文P至关于一个“处理器”,一个上下文链接一个或者多个Goroutine。为了运行goroutine,线程必须保存上下文。
上下文P(Processor)的数量在启动时设置为GOMAXPROCS
环境变量的值或经过运行时函数GOMAXPROCS()
。一般状况下,在程序执行期间不会更改。上下文数量固定意味着只有固定数量的线程在任什么时候候运行Go代码。咱们可使用它来调整Go进程到我的计算机的调用,例如4核PC在4个线程上运行Go代码。
图中P正在执行的Goroutine
为蓝色的;处于待执行状态的Goroutine
为灰色的,灰色的Goroutine
造成了一个队列runqueues
。
Go语言里,启动一个goroutine很容易:go function 就行,因此每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,一旦上下文运行goroutine直到调度点,它会从其runqueue中弹出goroutine,设置堆栈和指令指针并开始运行goroutine。
你可能会想,为何必定须要一个上下文,咱们能不能直接除去上下文,让Goroutine
的runqueues
挂到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中偷一半!
优势:
一、开销小
POSIX的thread API虽然可以提供丰富的API,例如配置本身的CPU亲和性,申请资源等等,线程在获得了不少与进程相同的控制权的同时,开销也很是的大,在Goroutine中则不需这些额外的开销,因此一个Golang的程序中能够支持10w级别的Goroutine。
每一个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少(goroutine:2KB ,线程:8MB)
二、调度性能好
在Golang的程序中,操做系统级别的线程调度,一般不会作出合适的调度决策。例如在GC时,内存必需要达到一个一致的状态。在Goroutine机制里,Golang能够控制Goroutine的调度,从而在一个合适的时间进行GC。
在应用层模拟的线程,它避免了上下文切换的额外耗费,兼顾了多线程的优势。简化了高并发程序的复杂度。
缺点:
协程调度机制没法实现公平调度。
参考: