CSP communicating sequential processes并发模型

目录
  • 前言
  • GO并发模型的实现原理
    • 内核级线程模型
    • 两级线程模型
  • Go线程实现模型MPG
  • Goroutine 小结
    • 优点:
    • 缺点:

前言

https://www.jb51.net/article/228730.htm

请记住下面这句话:

DO NOT COMMUNICATE BY SHARING MEMORY; INSTEAD, SHARE MEMORY BY COMMUNICATING.

“不要以共享内存的方式来通信,相反,要通过通信来共享内存。”

普通的线程并发模型,就是像Java、C++、或者Python,他们线程间通信都是通过共享内存的方式来进行的。非常典型的方式就是,在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,通过锁来访问,因此,在很多时候,衍生出一种方便操作的数据结构,叫做“线程安全的数据结构”。例如Java提供的包”java.util.concurrent”中的数据结构。Go中也实现了传统的线程并发模型。

Go的CSP并发模型,是通过goroutinechannel来实现的。

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 是关键字 代表我要创建一个通道

GO并发模型的实现原理

我们先从线程讲起,无论语言层面何种并发模型,到了操作系统层面,一定是以线程的形态存在的。而操作系统根据资源访问权限的不同,体系架构可分为用户空间和内核空间;内核空间主要操作访问CPU资源、I/O资源、内存资源等硬件资源,为上层应用程序提供最基本的基础资源,用户空间呢就是上层应用程序的固定活动空间,用户空间不可以直接访问资源,必须通过“系统调用”、“库函数”或“Shell脚本”来调用内核空间提供的资源。

我们现在的计算机语言,可以狭义的认为是一种“软件”,它们中所谓的“线程”,往往是用户态的线程,和操作系统本身内核态的线程(简称KSE),还是有区别的。

线程模型的实现,可以分为以下几种方式:

用户级线程模型

如图所示,多个用户态的线程对应着一个内核线程,程序线程的创建、终止、切换或者同步等线程工作必须自身来完成。它可以做快速的上下文切换。缺点是不能有效利用多核CPU。

内核级线程模型

这种模型直接调用操作系统的内核线程,所有线程的创建、终止、切换、同步等操作,都由内核来完成。一个用户态的线程对应一个系统线程,它可以利用多核机制,但上下文切换需要消耗额外的资源。C++就是这种。

两级线程模型

这种模型是介于用户级线程模型和内核级线程模型之间的一种线程模型。这种模型的实现非常复杂,和内核级线程模型类似,一个进程中可以对应多个内核级线程,但是进程中的线程不和内核线程一一对应;这种线程模型会先创建多个内核级线程,然后用自身的用户级线程去对应创建的多个内核级线程,自身的用户级线程需要本身程序去调度,内核级的线程交给操作系统内核去调度。

M个用户线程对应N个系统线程,缺点增加了调度器的实现难度。

Go语言的线程模型就是一种特殊的两级线程模型(GPM调度模型)。

Go线程实现模型MPG

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。

抛弃P(Processor)

你可能会想,为什么一定需要一个上下文,我们能不能直接除去上下文,让Goroutinerunqueues挂到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中偷一半!

Goroutine 小结

优点:

1、开销小

POSIX的thread API虽然能够提供丰富的API,例如配置自己的CPU亲和性,申请资源等等,线程在得到了很多与进程相同的控制权的同时,开销也非常的大,在Goroutine中则不需这些额外的开销,所以一个Golang的程序中可以支持10w级别的Goroutine。

每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少(goroutine:2KB ,线程:8MB)

2、调度性能好

在Golang的程序中,操作系统级别的线程调度,通常不会做出合适的调度决策。例如在GC时,内存必须要达到一个一致的状态。在Goroutine机制里,Golang可以控制Goroutine的调度,从而在一个合适的时间进行GC。

在应用层模拟的线程,它避免了上下文切换的额外耗费,兼顾了多线程的优点。简化了高并发程序的复杂度。

缺点:

协程调度机制无法实现公平调度。

以上就是CSP communicating sequential processes并发模型的详细内容,更多关于CSP并发模型的资料请关注我们其它相关文章!

(0)

相关推荐

  • C#代替go采用的CSP并发模型实现

    目录 CSP(Communicating sequential processes) 在Go中的CSP 协程(提升并发的利器) 线程 线程的开销 回归协程 协程的目的 C#中的协程 C#中的CSP Go协程与.NET协程的区别? 写在最后 说起Golang(后面统称为Go),就想到他的高并发特性,在深入一些就是 Goroutine.在大家被它优雅的语法和简洁的代码实现的高并发程序所折服时,其实C#/.NET也可以很容易的做到.今天我们来参照Go,来用C#实现它所采用的的CSP并发模型. CSP(

  • Go语言CSP并发模型goroutine channel底层实现原理

    目录 Go的CSP并发模型(goroutine + channel) 1.goroutine goroutine的优点: 2.channel 无缓存channel 有缓存channel 3.Go并发模型的底层实现原理 4.一个CSP例子 参考Go的CSP并发模型实现:M, P, G Go语言是为并发而生的语言,Go语言是为数不多的在语言层面实现并发的语言. 并发(concurrency):多个任务在同一段时间内运行. 并行(parallellism):多个任务在同一时刻运行. Go的CSP并发模

  • 分析Go语言中CSP并发模型与Goroutine的基本使用

    目录 一.并发实现模型 1.1.多进程 1.2.多线程 1.3.协程 二.共享内存与CSP 三.Goroutine 一.并发实现模型 1.1.多进程 在之前的文章当中我们曾经介绍过,进程是操作系统资源分配的最小单元.所以多进程是在操作系统层面的并发模型,因为所有的进程都是有操作系统的内核管理的.所以每个进程之间是独立的,每一个进程都会有自己单独的内存空间以及上下文信息,一个进程挂了不会影响其他进程的运行.这个也是多进程最大的优点,但是它的缺点也很明显. 最大的缺点就是开销很大,创建.销毁进程的开

  • CSP communicating sequential processes并发模型

    目录 前言 GO并发模型的实现原理 内核级线程模型 两级线程模型 Go线程实现模型MPG Goroutine 小结 优点: 缺点: 前言 https://www.jb51.net/article/228730.htm 请记住下面这句话: DO NOT COMMUNICATE BY SHARING MEMORY; INSTEAD, SHARE MEMORY BY COMMUNICATING. “不要以共享内存的方式来通信,相反,要通过通信来共享内存.” 普通的线程并发模型,就是像Java.C++.

  • Go语言CSP并发模型实现MPG

    目录 Golang调度机制 并发(concurrency)和并行(parallellism) Go的CSP并发模型 Go并发模型的实现原理 用户级线程模型 内核级线程模型 两级线程模型 Go线程实现模型MPG 抛弃P(Processor) 均衡的分配工作 Golang调度机制 最近抽空研究.整理了一下Golang调度机制,学习了其他大牛的文章.把自己的理解写下来.如有错误,请指正!!! golang的goroutine机制有点像线程池: 一.go 内部有三个对象: P对象(processor)

  • 示例剖析golang中的CSP并发模型

    目录 1. 相关概念: 2. CSP (通信顺序进程) 3. channel:同步&传递消息 4. goroutine:实际并发执行的实体 5. golang调度器 1. 相关概念: 用户态:当一个进程在执行用户自己的代码时处于用户运行态(用户态) 内核态:当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态),引入内核态防止用户态的程序随意的操作内核地址空间,具有一定的安全保护作用.这种保护模式是通过内存页表操作等机制,保证进程间的地址空间不会相互冲突,一个进程的操作不会修改另一个

  • Go语言并发模型的2种编程方案

    概述 我一直在找一种好的方法来解释 go 语言的并发模型: 不要通过共享内存来通信,相反,应该通过通信来共享内存 但是没有发现一个好的解释来满足我下面的需求: 1.通过一个例子来说明最初的问题 2.提供一个共享内存的解决方案 3.提供一个通过通信的解决方案 这篇文章我就从这三个方面来做出解释. 读过这篇文章后你应该会了解通过通信来共享内存的模型,以及它和通过共享内存来通信的区别,你还将看到如何分别通过这两种模型来解决访问和修改共享资源的问题. 前提 设想一下我们要访问一个银行账号: 复制代码 代

  • PHP下用Swoole实现Actor并发模型的方法

    什么是Actor? Actor对于PHPer来说,可能会比较陌生,写过Java的同学会比较熟悉,Java一直都有线程的概念(虽然PHP有Pthread,但不普及),它是一种非共享内存的并发模型,每个Actor内的数据独立存在,Actor之间通过消息传递的形式进行交互调度,且Actor是一种高度抽象化的编程模型,非常适合于游戏.硬件行业. Swoole协程与信箱 得益于Swoole4.x,我们可以基于Swoole的协程与Channel快速实现一个信箱模式调度.模拟代码如下: use Swoole\

  • Node.js 与并发模型的详细介绍

    目录 进程 线程 内核态线程 用户态线程 轻量级进程(LWP) 小结 协程 I/O 模型 阻塞 I/O 非阻塞 I/O 同(异)步 I/O Node.js 的并发模型 总结 前言: Node.js 现在已成为构建高并发网络应用服务工具箱中的一员,何以 Node.js 会成为大众的宠儿?本文将从进程.线程.协程.I/O 模型这些基本概念说起,为大家全面介绍关于 Node.js 与并发模型的这些事. 进程 我们一般将某个程序正在运行的实例称之为进程,它是操作系统进行资源分配和调度的一个基本单元,一般

  • java关于并发模型中的两种锁知识点详解

    1.悲观锁 悲观锁假设最坏的情况(如果果你不锁门,那么捣蛋鬼就会闯入并搞得一团糟),只有在确保其他线程不受干扰(获得正确的锁)的情况下才能执行. 一般实现如独占锁等. 安全性更高,但中低并发性效率更低. 2.乐观锁 乐观锁通过冲突检查机制判断更新过程中是否存在其他线程干扰.如果存在,操作将失败,重试(也可以不重试). CAS等常见实现. 一些乐观锁削弱了一致性,但在中低并发性下效率大大提高. 知识点扩展: 并行与分布式编程 关注的是复杂软件系统的构造,"复杂"是指多线程.分布式与GUI

随机推荐