本篇内容主要讲解“如何理解go并发机制及它所使用的CSP并发模型”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何理解go并发机制及它所使用的CSP并发模型”吧!
提到并发,就不得不提并行,这里我们先回顾下并发和并行的区别与联系:
并发(concurrency):两个或两个以上的任务在一段时间内被执行,我们不必关注这些任务在某一个时间点是否同时执行,可能同时执行,也可能不是,我们只关心在一段时间内,哪怕是很短的时间(一秒或者两秒)是否执行解决了两个或两个以上的任务
并行(parallellism):两个或两个以上的任务在同一时刻被同时执行
简而言之,并发说的是逻辑上的概念,而并行,强调的是物理运行状态。并发“包含”并行。
在计算机科学中,通信顺序过程(communicating sequential processes,CSP)是一种描述并发系统中交互模式的正式语言,它是并发数学理论家族中的一个成员,被称为过程算法(process algebras),或者说过程计算(process calculate),是基于消息的通道传递的数学理论。
CSP模型是上个世纪70年代提出的,不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。CSP是用于描述两个独立的并发实体通过共享的通讯即channel(管道)进行通信的并发模型。在CSP中channel是第一类对象,CSP并不关注发送消息的实体,它只关注实体之间发送消息时使用的channel。
Go中的channel是被单独创建并且可以在进程之间进行消息传递的结构体,它的通信模式类似于boss-worker模式,一个实体将消息发送到channel中,然后由另一个实体监听并接收channel的消息,在这一过程中,两个实体之间是匿名的,这个就实现了实体之间的解耦。
Go实现了两种并发方式。第一种是大家大家普遍认知的:多线程共享内存。其实就是Java或者C++等语言中的多线程开发。另外一种是Go语言特有的,也是Go语言推荐的:CSP并发模型。
普通的线程并发模型,就是像Java、C++、或者Python,他们线程间通信都是通过共享内存的方式来进行的。
非常典型的方式就是,在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,通过锁来访问。
因此,在很多时候,衍生出一种方便操作的数据结构,叫做“线程安全的数据结构”。例如Java提供的包”java.util.concurrent”中的数据结构。
Go中也实现了传统的线程并发模型,如为了在并发场景中安全使用map,Go提供了sync.Map。
Go的CSP并发模型,是通过goroutine和channel来实现的。
goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“
channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道
goroutine底层使用协程(coroutine)实现并发,coroutine是一种运行在用户态的用户线程,类似于greenthread。
go选择使用coroutine的出发点是因为:
用户空间,避免了内核态和用户态的切换导致的成本
可以由语言和框架层进行调度
更小的栈空间允许创建大量实例
生成一个goroutine的方式非常的简单:
go f()
通信机制channel也很方便,传数据用channel <- data,取数据用<-channel。
在通信过程中,传数据channel <- data和取数据<-channel必然会成对出现,因为这边传,那边取,两个goroutine之间才会实现通信。
而且不管传还是取,必阻塞,直到另外的goroutine传或者取为止。
以流程图形式展示这一过程如下:
假设有两个goroutine,其中一个首先向channel发送了数据:(goroutine为矩形,channel为箭头)
左边的goroutine开始阻塞,等待有人从channel接收数据。
然后右边的goroutine发起了接收操作。
右边的goroutine也开始阻塞,等待有人向cahnnel发送数据。
这时候,两边goroutine都发现了对方,于是两个goroutine就通过channel实现了数据的传输。
我们先从线程讲起,无论语言层面是何种并发模型,到了操作系统层面,一定是以线程的形态存在的。而操作系统根据资源访问权限的不同,体系架构可分为用户空间和内核空间;内核空间主要操作访问CPU资源、I/O资源、内存资源等硬件资源,为上层应用程序提供最基本的基础资源,用户空间则就是上层应用程序的固定活动空间,用户空间不可以直接访问资源,必须通过“系统调用”、“库函数”或“Shell脚本”来调用内核空间提供的资源。
我们现在的计算机语言,可以狭义的认为是一种“软件”,它们中所谓的“线程”,往往是用户态的线程,和操作系统本身内核态的线程(简称KSE),还是有区别的。
线程模型的实现,可以分为以下几种方式:
用户级线程模型
如图所示,多个用户态的线程对应着一个内核线程,程序线程的创建、终止、切换或者同步等线程工作必须自身来完成。
内核级线程模型
这种模型直接调用操作系统的内核线程,所有线程的创建、终止、切换、同步等操作,都由内核来完成。C++就是这种。
两级线程模型
这种模型是介于用户级线程模型和内核级线程模型之间的一种线程模型。
这种模型的实现非常复杂:
和内核级线程模型类似,一个进程中可以对应多个内核级线程,但是进程中的线程不和内核线程一一对应;
这种线程模型会先创建多个内核级线程,然后用自身的用户级线程去对应创建的多个内核级线程;
自身的用户级线程需要本身程序去调度,内核级的线程交给操作系统内核去调度。
Go语言的线程模型就是一种特殊的两级线程模型。暂且叫它“GPM”模型吧。
GPM简介
G指的是Goroutine,其实本质上也是一种轻量级的线程。
P指的是”processor”,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。
M指的是Machine,一个M直接关联了一个内核线程。
三者关系如下图所示:
上图讲的是两个线程(内核线程)的情况。一个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中偷一半。
到此,相信大家对“如何理解go并发机制及它所使用的CSP并发模型”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。