大家好,今天将梳理出的 Go语言并发知识内容,分享给大家。 请多多指教,谢谢。
本次《Go语言并发知识》内容共分为三个章节,本文为第二章节。
- Golang 基础之并发知识 (一)
- Golang 基础之并发知识 (二)
- Golang 基础之并发知识 (三)
本章节内容
- GMP 模型
- 通信顺序进程模式
- 多线程共享内存模式
GMP 模型
知识扩展:Golang运行时是有一个运行时(runtime)。运行时在用户空间而不是内核中执行计划任务,因此它更轻量级。它在系统资源使用和性能之间做了更好的权衡,尤其是在IO任务中。
runtime 内容后续章节在为大家介绍。
接下来,我将向大家介绍下 GMP的设计模式。
在GMP模式中,线程是物理生产者,调度器会将goroutine分派给一个线程。
说明:
- 全局队列:要执行goroutines的队列
- P 本地队列:与全局队列一样,它包含要执行的goroutine;但是这个队列的最大容量是256。当本地队列已满时,会将gorouting的一半发送到全局队列;最后,G创建的Goroutine 将在同一个队列中排队,以确保本地关联
- P 列表:一旦确定GOMAXPROCS的值,所有P的列表都将被创建
- M 正在运行的线程:从P的本地队列获取任务,如果该队列为空,M将从全局队列获取goroutines到P的本地队列,或者从其他P的队列获取goroutines。G在M中运行,当任务完成时,它将继续拉一个新的G
Goroutine调度程序和OS调度程序使用M连接,每个M是一个物理OS线程,OS调度程序调度M运行在一个真正的CPU内核中。
GMP描述
- G: goroutine,可以理解为协程
- M:线程,内核线程,goroutine跑在M之上的 ( M的数量在
runtime
/debug
包的SetMaxThreads()
决定。如果当前的M阻塞,就会新建一个新的线程。) - P:调度器,goroutine的队列 (P的数量由
GOMAXPROCS
环境变量,或者runtime
中GOMAXPROCS()
函数决定的。)
注意:M的数量和P的数量没有关系。如果当前的M阻塞,P的goroutine会运行在其他的M上,或者新建一个M。所以可能出现有很多个M,只有1个P的情况。
MP创建周期
- P:在确定P的数量后,(runtime)运行时将创建P。
- M:如果没有足够的M来执行P的任务,它将被创建。(所有M都被阻止,将创建新的M来运行P的任务)
调度器机制
调度过程
- 首先创建一个G对象,然后G被保存在P的本地队列或者全局队列(global queue),这时P会唤醒一个M。
- P按照它的执行顺序继续执行任务。M寻找一个空闲的P,如果找得到,将G移动到它自己。
- 然后M执行一个调度循环:调用G对象->执行->清理线程->继续寻找Goroutine。
调度策略
Reuse(重用): 重用线程以避免频繁创建、销毁线程。
- Work Stealing(工作窃取): 当没有G可以运行时,从P绑定的其他M处盗取G,而不是销毁
- Hand Off(切换): 当P被阻塞时,将其转移到其他自由M
- Concurrency(并发):至少有更多的GoMaxProc goroutines同时运行。但是,如果GOMACPROCS<CPU内核,它还设置了并发限制。
- Preemptive(抢占):协同程序必须放弃cpu时间。但是在golang,一个goroutine每次最多可以运行10毫秒,以避免其他goroutine饥饿。
- Global Goroutines Queue(全局goroutine队列):当工作窃取失败时,M可以从全局队列中拉出goroutines。
goroutine实现
go func() 触发流程
说明
go func(){}
创建一个新的goroutine
。- G保存在P的本地队列,如果本地队列满了,保存在全局队列。
- G在M上运行,每个M绑定一个P。如果P的本地队列没有G,M会从其他P的本地队列,挥着G的全局队列,窃取G。
- 当M阻塞时,会将M从P解除。把G运行在其他空闲的M或者创建新的M。
- 当M恢复时,会尝试获得一个空闲的P。如果没有P空闲,M会休眠,G会放到全局队列。
生命周期
流程说明
- runtime创建M0,G0
- 调度器初始化:初始化M0,栈,垃圾回收,创建初始的长度为
GOMAXPROCS
的P列表 runtime.main
创建代码的main.main
,创建主goroutine,然后放到P的本地队列- 启动M0, M0绑定P
- 根据goroutine的栈和调度信息,M0设置运行环境
- 在M中运行G
- G退出,
runtime.main
调用defer
,panic
,最后调用runtime.exit
通信顺序进程模式
介绍
Communicating Sequential Processes,简称CSP,通信顺序进程。 是由 Antony Hoare 在1978年提出的概念,并在计算机协会(更通俗地称为ACM)上发表了这篇论文。
作者,Antony Hoare 托尼·霍尔 1980年获得图灵奖
详细了解请看他的 资料
在该论文中,Hoare认为输入和输出是两个被忽视的编程原语,特别是在并发代码中。在 Hoare 撰写本文时,关于如何构造程序的研究仍在进行中,但大部分工作都是针对连续代码的技术如:goto语句的使用正在讨论中,面向对象的思想开始萌发。并发并没有得到太多关注。 Hoare于是在论文中提出了使用进程间通信来使对于线程的操作以顺序形式编写的语言:CSP,这个时候还只是一个存在于论文中的编程语言。
Hoare 的 CSP 程序设计语言包含原型来正确模拟输入和输出或进程之间的通信。Hoare将术语 “进程” 应用于逻辑的所有封装部分,这些部分需要输入来运行并产生其他过程将消耗的输出。并且Hoare开创性地将进程这个概念运用到任何包含需要输入来运行且产生其他进程将会消费的输出的逻辑片段。
在golang发布之前,很少有语言能够真正的为这些原语提供支持。golang是最早将CSP的原则纳入其核心的语言之一,并将这种编程风格引入到大众中。大多数流行的语言都倾向于共享内存和同步对CSP的信息传递风格的访问。
内存访问同步本质上并不坏。 有时在某些情况下共享内存是合适的,即使在Go中也是如此。 但是,共享内存模型可能难以正确使用——特别是在大型或复杂的程序中。 正是因为这个原因,并发才被认为是Go的优势之一,它从一开始就以CSP的原则为基础,因此很容易阅读、编写和推理。
Go中的理解
Golang只使用了CSP当中关于Process/Channel的部分。简单来说Process映射Goroutine,Channel映射Channel。Goroutine即Golang当中的协程,Goroutine之间没有任何耦合,可以完全并发执行。Channel用于给Goroutine传递消息,保持数据同步。虽然Goroutine之间没有耦合,但是它们与Channel依然存在耦合。
整个Goroutine和 channel
的结构有些类似于生产消费者模式,多个线程之间通过队列共享数据,从而保持线程之间独立。
多线程共享内存模式
在消息传递之外,还存在一种广为人知的并发模型,那就是共享内存。其实如果不能共享内存,消息传递也是不能在不同的线程间传递消息,也谈不上在不同的线程间等待和通知了。共享内存是这一切得以发生的基础。如果查看源码,你会发现消息传递的内部实现就是借用了共享内存机制。相对于消息传递而言,共享内存会有更多的竞争,但是不用进行多次拷贝,在某些情况下,也需要考虑使用这种方式来处理。
采用共享内存并发,使得线程安全必须满足两个条件:内存可见性、原子性
内存可见性要保证两点:1、线程修改后的共享变量更新到主内存;2、从主内存中更新最新值到工作内存中;
原子性:当线程引用共享变量时,工作内存中没有共享变量时它会从主内存复制共享变量到自己工作内存中,当工作内存有共享变量时线程可能会从主内存更新也有可能直接使用工作内存中的共享变量;
在Go语言设计思想中,推荐:不要以共享内存的方式来通信,相反,要通过通信来共享内存。
技术文章持续更新,请大家多多关注呀~~
搜索微信公众号【 帽儿山的枪手 】,关注我
参考材料
《Concurrency in Go》书籍
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。