Go语言并发机制
Go语言中的并发
使用goroutine编程
使用 go 关键字用来创建 goroutine 。将go声明放到一个需调用的函数之前,在相同地址空间调用运行这个函数,这样该函数执行时便会作为一个独立的并发线程。这种线程在Go语言中称作goroutine。
goroutine的用法如下:
//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并发模型,是通过goroutine
和channel
来实现的。
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)
你可能会想,为什么一定需要一个上下文,我们能不能直接除去上下文,让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中偷一半!
参考:
https://yq.aliyun.com/articles/72365
http://morsmachine.dk/go-scheduler
https://studygolang.com/articles/11825
Go语言并发机制的更多相关文章
- Go语言并发机制初探
Go 语言相比Java等一个很大的优势就是可以方便地编写并发程序.Go 语言内置了 goroutine 机制,使用goroutine可以快速地开发并发程序, 更好的利用多核处理器资源.这篇文章学习 g ...
- Go语言并发
Go语言并发机制初探 Go 语言相比Java等一个很大的优势就是可以方便地编写并发程序.Go 语言内置了 goroutine 机制,使用goroutine可以快速地开发并发程序, 更好的利用多核处 ...
- (第二章)Java并发机制的底层实现原理
一.概述 Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令. ...
- 【Java基础】线程和并发机制
前言 在Java中,线程是一个很关键的名词,也是很高频使用的一种资源.那么它的概念是什么呢,是如何定义的,用法又有哪些呢?为何说Android里只有一个主线程呢,什么是工作线程呢.线程又存在并发,并发 ...
- (转)Go语言并发模型:使用 context
转载自:https://segmentfault.com/a/1190000006744213 context golang 简介 在 Go http包的Server中,每一个请求在都有一个对应的 g ...
- Java并发机制和底层实现原理
Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码转化为汇编指令在CPU上执行.Java中的并发机制依赖于JVM的实现和CPU的指令. Java语言规范第三版中 ...
- go语言之进阶篇并行和并发的区别与go语言并发优势
1.并行和并发的概念 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行. 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在 ...
- 【java并发编程艺术学习】(三)第二章 java并发机制的底层实现原理 学习记录(一) volatile
章节介绍 这一章节主要学习java并发机制的底层实现原理.主要学习volatile.synchronized和原子操作的实现原理.Java中的大部分容器和框架都依赖于此. Java代码 ==经过编译= ...
- 09. Go 语言并发
Go 语言并发 并发指在同一时间内可以执行多个任务.并发编程含义比较广泛,包含多线程编程.多进程编程及分布式程序等.本章讲解的并发含义属于多线程编程. Go 语言通过编译器运行时(runtime),从 ...
随机推荐
- ResourceUtils 创建资源目录工具类
package com.jcf.utilsdemo; import android.content.Context; import android.content.res.Resources; pub ...
- 查看openstack各组件小版本号
查看api版本号 因为要判断openstack的某个小功能是否支持,需要确定api版本号大于某个版本,故需要查询现有环境api版本号 如查看cinder-api的版本号 指定api版本调api 在he ...
- php连接数据库的两种方式- 面向过程 面向对象
php连接数据库的两种方式- 面向过程 面向对象 一.面向对象1. 链接数据库$conn = @new mysqli("127.0.0.1","root", ...
- vue指令实现拖动的高级写法
不熟悉vue自定义指令看这里: https://cn.vuejs.org/v2/guide/custom-directive.html vue指令实现拖动方法很方便也挺简单,但是网上大部分的教程代码, ...
- 九十四:CMS系统之cms后台登录限制
装饰器,验证当前session中是否存在定义的user_id,没有就重定向到登录页 from flask import session, redirect, url_forfrom functools ...
- salt收集windows服务器日志时间
定义收集时间区间: time.ps1 $a = get-date $b = $a.adddays(-) $c = $b.tostring('yyyy-MM-dd') $d = get-date -fo ...
- charles修改请求体内容
charles修改请求体内容 问:什么是请求体?答:客户端向服务端发出的请求简称请求体,请求体中包含有许许多多的参数,每一个参数都有其特定的意义.多一个或者少一个则服务端给你返回的响应体就会不一样 一 ...
- 【POJ - 1950】Dessert(dfs)
-->Dessert Descriptions: 给你一个数N(3<=N<=15);每个数之间有三种运算符“‘+’,‘-’,‘.’”.输出和值等于零的所有的运算情况及次数num,如果 ...
- 【并行计算-CUDA开发】有关CUDA当中global memory如何实现合并访问跟内存对齐相关的问题
ps:这是英伟达二面面的一道相关CUDA的题目.<NVIDIA CUDA编程指南>第57页开始 在合并访问这里,不要跟shared memory的bank conflic ...
- 【转帖】Ubuntu : apt-get 命令
Ubuntu : apt-get 命令 https://www.cnblogs.com/sparkdev/p/11339231.html ubuntu的 我一直不熟 感谢作者 总结的这么好 在学习一下 ...