写点什么

Go 并发原理

2019 年 9 月 23 日

Go并发原理

1. 背景

Go 语言是为并发而生的语言,Go 语言是为数不多的在语言层面实现并发的语言;也正是 Go 语言的并发特性,吸引了全球无数的开发者。


2. 并发和并行

并发(concurrency):两个或两个以上的任务在一段时间内被执行。我们不必 care 这些任务在某一个时间点是否是同时执行,可能同时执行,也可能不是,我们只关心在一段时间内,哪怕是很短的时间(一秒或者两秒)是否执行解决了两个或两个以上任务。


并行(parallellism):两个或两个以上的任务在同一时刻被同时执行。


并发说的是逻辑上的概念,而并行,强调的是物理运行状态。并发“包含”并行。


3. 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 一下,就生成了。


1go f();
复制代码


通信机制 channel 也很方便,传数据用 channel <- data,取数据用<-channel。


在通信过程中,传数据 channel <- data 和取数据<-channel 必然会成对出现,因为这边传,那边取,两个 goroutine 之间才会实现通信。


而且不管传还是取,必阻塞,直到另外的 goroutine 传或者取为止。


有两个 goroutine,其中一个发起了向 channel 中发起了传值操作。(goroutine 为矩形,channel 为箭头)


左边的 goroutine 开始阻塞,等待有人接收。


这时候,右边的 goroutine 发起了接收操作。


右边的 goroutine 也开始阻塞,等待别人传送。


这时候,两边 goroutine 都发现了对方,于是两个 goroutine 开始一传,一收。



这便是 Golang CSP 并发模型最基本的形式。


4. Go 并发模型的实现原理

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


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


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


1)用户级线程模型



如图所示,多个用户态的线程对应着一个内核线程,程序线程的创建、终止、切换或者同步等线程工作必须自身来完成。


2)内核级线程模型



这种模型直接调用操作系统的内核线程,所有线程的创建、终止、切换、同步等操作,都由内核来完成。C++就是这种。


3)两级线程模型



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


Go 语言的线程模型就是一种特殊的两级线程模型。暂且叫它“MPG”模型吧。


5. Go 线程实现模型 MPG

M 指的是 Machine,一个 M 直接关联了一个内核线程。


P 指的是"processor",代表了 M 所需的上下文环境,也是处理用户级代码逻辑的处理器。


G 指的是 Goroutine,其实本质上也是一种轻量级的线程。


三者关系如下图所示:



以上这个图讲的是两个线程(内核线程)的情况。一个 M 会对应一个内核线程,一个 M 也会连接一个上下文 P,一个上下文 P 相当于一个“处理器”,一个上下文连接一个或者多个 Goroutine。P(Processor)的数量是在启动时被设置为环境变量 GOMAXPROCS 的值,或者通过运行时调用函数 runtime.GOMAXPROCS()进行设置。Processor 数量固定意味着任意时刻只有固定数量的线程在运行 go 代码。Goroutine 中就是我们要执行并发的代码。图中 P 正在执行的 Goroutine 为蓝色的;处于待执行状态的 Goroutine 为灰色的,灰色的 Goroutine 形成了一个队列 runqueues


三者关系的宏观的图为:



  1. 抛弃 P(Processor)

  2. 你可能会想,为什么一定需要一个上下文,我们能不能直接除去上下文,让 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 可能得不到执行而饿死。


7. 均衡的分配工作

按照以上的说法,上下文 P 会定期的检查全局的 goroutine 队列中的 goroutine,以便自己在消费掉自身 Goroutine 队列的时候有事可做。假如全局 goroutine 队列中的 goroutine 也没了呢?就从其他运行的中的 P 的 runqueue 里偷。


每个 P 中的 Goroutine 不同导致他们运行的效率和时间也不同,在一个有很多 P 和 M 的环境中,不能让一个 P 跑完自身的 Goroutine 就没事可做了,因为或许其他的 P 有很长的 goroutine 队列要跑,得需要均衡。


该如何解决呢?


Go 的做法倒也直接,从其他 P 中偷一半!



参考文献:

The Go scheduler

《Go 并发编程第一版》


作者介绍:


刘刚,贝壳找房研发工程师,目前负责贝壳找房运维开发工作。


本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。


原文链接:


https://mp.weixin.qq.com/s/nlaRii1AWwn0QJDRe-EoVQ


2019 年 9 月 23 日 08:00488

评论

发布
暂无评论
发现更多内容

2021 DevOpsDays 东京站完美收官 | CODING 专家受邀分享最新技术资讯

CODING DevOps

CI/CD DevOpsDays CODING DevOps

活久见!22张图带你maven实战通关,GitHub上线1小时浏览量破万

java专业爱好者

Java

iOS面试--拼多多最新iOS开发面试题

一意孤行的程序员

ios swift 面试 ios开发 知识分享

字节跳动Java岗一二三面全经过分享

北游学Java

Java 字节跳动 面试

技术干货 | 基于MindSpore更好的理解Focal Loss

华为云开发者社区

函数 mindspore Focal Loss 样本

Nginx的11个执行阶段详解

运维研习社

nginx 运维 源码剖析 5月日更

fil挖矿步骤教程是什么?fil挖矿规则是什么?

投资矿机v:IPFS1234

fil挖矿步骤教程是什么 fil挖矿规则是什么

极光开发者周刊【No.0521】

极光开发者

新思科技为中兴通讯提供BSIMM软件安全评估

InfoQ_434670063458

5G 新思科技 中兴 软件安全 BSIMM

☕【JVM技术之旅】你真正掌握了Java对象创建的流程吗?

李浩宇/Alex

JVM java对象分析 java对象 对象创建 5月日更

LRU 和 LFU

且听且吟

M1 Dock智能硬件环境搭建(MaixPy安装及使用)

不脱发的程序猿

人工智能 开发板 智能硬件 AIOT M1 Dock

「DataPipeline」完成数千万B轮融资,加速构建中国的世界级数据中间件产品

DataPipeline数见科技

融资

阿里P9架构师强烈推荐:想拿60W以上年薪必看,Java高并发四套小册。

Java架构追梦

Java 阿里巴巴 架构 面试 高并发

并发王者课 - 青铜 3: 双刃剑-理解多线程带来的安全问题

技术八点半

Java 多线程 并发 王者并发课

Hive窗口函数与分析函数

大数据技术指南

hive 5月日更

云小课|聊一聊DRS的数据过滤特性

华为云开发者社区

数据库 DRS 数据复制服务 数据过滤 数据库引擎

GitHub开源的AI下五子棋

不脱发的程序猿

人工智能 GitHub 开源项目 五子棋 AI五子棋

「技术人生」第2篇:学会分析事物的本质

阿里巴巴中间件

技术 工具 技术人 技术人生 一号位

这么狠,私塾在线架构师系列课程全都免费发放

InfoQ_d2212957090d

Java

MindSpore:不用摘口罩也知道你是谁

华为云开发者社区

算法 人脸识别 口罩 mindspore 口罩人脸

2021年中国信创生态报告发布 指引未来信创产业发展

融云 RongCloud

Spark知识点简单总结

五分钟学大数据

大数据 spark 5月日更

每个开发人员都应该知道的 10 个 GitHub 仓库

LeanCloud

GitHub web开发

BMP、GIF、TIFF、PNG、JPG和SVG格式图像的特点

不脱发的程序猿

图像格式

重点人员管控监控平台建设,重点人口预警系统开发方案

WX13823153201

我粉了!阿里大牛从内部带出来的百亿级高并发系统,从基础到实战、面面俱到

云流

Java 程序员 架构 面试

做一次黑客,入侵一次服务器

叫我阿柒啊

Docker 入侵 docker远程 redis注入

记十亿级Es数据迁移mongodb成本节省及性能优化实践

杨亚洲(专注mongodb及高性能中间件)

MySQL 数据库 mongodb 架构 分布式数据库mongodb

量化马丁策略交易系统开发app,现货量化交易平台搭建

WX13823153201

老生常谈:面试必问“三次握手,四次挥手”这么讲,保证你忘不了

程序员小毕

Java 程序员 面试 TCP 网络

Go并发原理-InfoQ