本文视频地址

Go 语言原生并发原则

1) Go 语言自身实现层面支持面向多核硬件的并发执行和调度

提到并发执行与调度,我们首先想到的就是操作系统对进程、线程的调度。操作系统调度器会将系统中的多个线程按照一定算法调度到物理 CPU 上去运行。传统的编程语言比如 C、C++ 等的并发实现实际上就是基于操作系统调度的,即程序负责创建线程(一般通过 pthread 等函数库调用实现),操作系统负责调度。这种传统支持并发的方式有诸多不足:

复杂

  • 1 创建容易,退出难:使用 C 语言的开发人员都知道,创建一个 thread(比如利用 pthread)虽然参数也不少,但还可以接受。但一旦涉及到 thread 的退出,就要考虑 thread 是 detached,还是需要 parent thread 去 join?是否需要在 thread 中设置 cancel point,以保证 join 时能顺利退出?

  • 2 并发单元间通信困难,易错:多个 thread 之间的通信虽然有多种机制可选,但用起来是相当复杂;并且一旦涉及到 shared memory,就会用到各种 lock,死锁便成为家常便饭.

  • 3 thread stack size 的设定:是使用默认的,还是设置的大一些,或者小一些呢?

难于扩展

  • 1 一个 thread 的代价已经比进程小了很多了,但我们依然不能大量创建 thread,因为除了每个 thread 占用的资源不小之外,操作系统调度切换 thread 的代价也不小;

  • 2 对于很多网络服务程序,由于不能大量创建 thread,就要在少量 thread 里做网络多路复用,即:使用 epoll/kqueue/IoCompletionPort 这套机制,即便有 libevent、libev 这样的第三方库帮忙,写起这样的程序也是很不易的,存在大量 callback,给程序员带来不小的心智负担。

为此,Go 采用了用户层轻量级 thread或者说是类 coroutine的概念来解决这些问题,Go 将之称为"goroutine"。goroutine 占用的资源非常小,每个 goroutine stack 的 size 默认设置是 2k,goroutine 调度的切换也不用陷入(trap)操作系统内核层完成,代价很低。因此,一个 Go 程序中可以创建成千上万个并发的 goroutine。所有的 Go 代码都在 goroutine 中执行,哪怕是 go 的 runtime 也不例外。将这些 goroutines 按照一定算法放到“CPU”上执行的程序就称为goroutine 调度器或goroutine scheduler。

不过,一个 Go 程序对于操作系统来说只是一个用户层程序,对于操作系统而言,它的眼中只有 thread,它甚至不知道有什么叫 Goroutine 的东西的存在。goroutine 的调度全要靠 Go 自己完成,实现 Go 程序内 goroutine 之间“公平”的竞争“CPU”资源,这个任务就落到了 Go runtime 头上。
Go 语言实现了G-P-M 调度模型和 work stealing 算法,这个模型一直沿用至今,如下图所示:


G:表示 goroutine,存储了 goroutine 的执行 stack 信息、goroutine 状态以及 goroutine 的任务函数等;另外 G 对象是可以重用的。

P:表示逻辑 processor,P 的数量决定了系统内最大可并行的 G 的数量(前提:系统的物理 cpu 核数>=P 的数量);P 的最大作用还是其拥有的各种 G 对象队列、链表、一些 cache 和状态。每个 G 要想真正运行起来,首先需要被分配一个 P(进入到 P 的 local runq 中)。对于 G 来说,P 就是运行它的“CPU”,可以说:G 的眼里只有 P。

M:M 代表着真正的执行计算资源,一般对应的是操作系统的线程。从 Goroutine 调度器的视角来看,真正的“CPU”是 M,只有将 P 和 M 绑定才能让 P 的 runq 中 G 得以真实运行起来。这样的 P 与 M 的关系,就好比 Linux 操作系统调度层面用户线程(user thread)与核心线程(kernel thread)的对应关系那样(N x M)。M 在绑定有效的 P 后,进入 schedule 循环;而 schedule 循环的机制大致是从各种队列、p 的本地队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 goexit 做清理工作并回到 m,如此反复。M 并不保留 G 状态,这是 G 可以跨 M 调度的基础。

2) Go 语言为开发者提供的支持并发的语法元素和机制

我们先来看看那些设计并诞生于单核年代的编程语言,诸如:C、C++、Java 在语法元素和机制层面是如何支持并发的。

  • 执行单元:线程;
  • 创建和销毁的方式:调用库函数或调用对象方法;
  • 并发线程间的通信:多基于操作系统提供的 IPC 机制,比如:共享内存、Socket、Pipe 等,当然也会使用有并发保护的全局变量。
    和上述传统语言相比,Go 为开发人员提供了语言层面内置的并发语法元素和机制:
  • 执行单元:goroutine;
  • 创建和销毁方式:go+函数调用;函数退出即 goroutine 退出;
  • 并发 goroutine 的通信:通过语言内置的 channel 传递消息或实现同步,并通过 select 实现多路 channel 的并发控制。
    对比来看,Go 对并发的原生支持将大大降低开发人员在开发并发程序时的心智负担。

3) 并发原则对 Go 开发者在程序结构设计层面的影响

由于 goroutine 的开销很小(相对线程),Go 官方是鼓励大家使用 goroutine 来充分利用多核资源的。但并不是有了 goroutine 就一定能充分的利用多核资源,或者说即便使用 Go 也不一定能设计编写出一个好的并发程序。
为此 Rob Pike 曾有过一次关于“并发不是并行”1的主题分享,在那次分享中,这位 Go 语言之父图文并茂地讲解了并发(Concurrency)和并行(Parallelism)的区别。Rob Pike 认为:

  • 并发是有关结构的,它是一种将一个程序分解成小片段并且每个小片段都可以独立执行的程序设计方法; 并发程序的小片段之间一般存在通信联系并且通过通信相互协作;
  • 并行是有关执行的,它表示同时进行一些计算任务 。

重点:并发是一种程序结构设计的方法,它使得并行成为可能。不过这依然很抽象,我们这里也借用 Rob Pike 分享中的那个“搬运书问题”来重新诠释一下并发的含义。搬运书问题要求设计一个方案,使得 gopher 能更快地将一堆废弃的语言手册搬到垃圾回收场烧掉。

这显然不是并发设计方案,它没有对问题进行任何分解,所有事情都是由一个 gopher 从头到尾按顺序完成的。但即便这样一个并非并发的方案,我们也可以将其放到多核的硬件上并行的执行,只是需要多建立几个 gopher 例程(procedure)的实例罢了:
这个方案显然不是并发设计方案,它没有对问题进行任何分解,所有事情都是由一个 gopher 从头到尾按顺序完成的。但即便这样一个并非并发的方案,我们也可以将其放到多核的硬件上并行的执行,只是需要多建立几个 gopher 例程(procedure)的实例罢了:

但和并发方案相比,这种方案是缺乏自动扩展为并行的能力的。Rob Pike 在分享中给出了两种并发方案,也就是该问题的两种分解方案,两种方案都是正确的,只是分解粒度的细致程度不同。
方案 1 将原来单一的 gopher 例程执行拆分为 4 个执行不同任务的 gopher 例程,每个例程更简单:

  • 将书搬运到车上(loadBooksToCart);
  • 推车到垃圾焚化地点(moveCartToIncinerator);
  • 将书从车上搬下送入焚化炉(unloadBookIntoIncinerator);
  • 将空车送返(returnEmptyCart)。
    理论上并发方案 1 的处理性能能达到初始方案的四倍,并且不同 gopher 例程可以在不同的处理器核上并行执行,而无需像最初方案那样需要建立新实例实现并行。

    方案 2 增加了“暂存区域”,分解的粒度更细,每个部分的 gopher 例程各司其责,这样的程序在单核处理器上也是正常运行的(在单核上可能处理能力不如非并发方案)。但随着处理器核数的增多,并发方案可以自然地提高处理性能,提升吞吐。而非并发方案在处理器核数提升后,也仅仅能使用其中的一个核,无法自然扩展,这一切都是程序的结构所决定的。这也告诉我们:并发程序的结构设计不要局限于在单核情况下处理能力的高低,而是以在多核情况下能够充分提升多核利用率、获得性能的自然提升为最终目的。

03.Go语言的设计哲学之三: 并发相关推荐

  1. Golang 入门 : Go语言的设计哲学

    前言 设计哲学之于编程语言,就好比一个人的价值观之于这个人的行为. 因为如果你不认同一个人的价值观,那你其实很难与之持续交往下去,即所谓道不同不相为谋.类似的,如果你不认同一门编程语言的设计哲学,那么 ...

  2. 【无为则无心Python基础】— 2、Python语言的设计哲学

    文章目录 1.Python 的设计哲学 2.Python 的设计目标 先总体概括Python的设计哲学就是:优雅.明确.简单. 1.Python 的设计哲学 为了更好的了解Python哲学理念及设计思 ...

  3. Python(1)-源起、设计目标、设计哲学、特点

    python简介 1. python的起源 2. 解释器 3. python 语言的设计目标 4. python 语言的设计哲学 5. Python 特点 人生苦短,我用python–吉多·范罗苏姆( ...

  4. Rust语言入门(2)——设计哲学

    设计哲学 1 简述 任何一门语言的兴起,都是为了解决一个问题. 自操作系统诞生以来,系统级主流变成语言,从汇编语言到C++, 已经发展了近50年.但仍然存在两个难题: 很难编写内存安全的代码 很难编写 ...

  5. e语言通用进销存源码_Go 语言设计哲学之五:代码风格的唯一标准

    一. gofmt Go 语言设计的目标之一就是解决大型软件系统的大规模开发的问题,解决大型团队的开发问题,Go 核心团队给它起了一个名字叫:规模化(scale). gofmt 是伴随着 Go 语言诞生 ...

  6. Kotlin语言中的泛型设计哲学

    Kotlin语言的泛型设计很有意思,但并不容易看懂.关于这个部分的官方文档,我反复看了好几次,终于弄明白Kotlin语言泛型设计的背后哲学.这篇文章将讲述Kotlin泛型设计的整个思考过程及其背后的哲 ...

  7. 小啊呜产品读书笔记001:《邱岳的产品手记-02》 开篇词 产品经理的世界没有对错 01讲 验证码是个好设计吗 02讲 产品经理工具指南 03讲 产品案例分析·Trigraphy的设计哲学

    小啊呜产品读书笔记001:<邱岳的产品手记-02> 开篇词 产品经理的世界没有对错 & 01讲 验证码是个好设计吗 & 02讲 产品经理工具指南 & 03讲 产品案 ...

  8. Cloud Native的设计哲学理念,kubernetes云生态操作系统

    本文作者:行癫 2015年的时候google就提出了云原生计算基金会(CNCF),最开始的时候我对于云原生的定义于以下几个方面:应用容器化,面向微服务架构,应用支持容器的调度.但是这种想法和定义在未来 ...

  9. 独家解读:魅族数据平台的设计哲学和核心架构

    由最初的纯设备生产厂商渐渐发展为今天的智能设备设计商和互联网服务提供商,"魅族"转眼间已经历了十年的成长,其Flyme系统也在逐渐走向成熟.而依托于Flyme的魅族互联网服务也逐渐 ...

最新文章

  1. linux网络驱动架构,Linux网络体系架构和网卡驱动设计
  2. 开源轻量级办公系统Sandbox介绍以及配套开发文档连载
  3. 【Linux Deploy】一、Linux Deploy安装配置使用教程
  4. pythonweb开发-手把手教你写网站:Python WEB开发技术实战
  5. SQLSERVER自动定时(手动)备份工具
  6. w8计算机配置要求,win8系统最低配置要求有哪些|win8系统是否有最低配置要求-系统城...
  7. 将 SharePoint 开发与其他形式的开发进行比较
  8. VMware虚拟机扩展Ubuntu系统磁盘空间
  9. [转载].NET平台微服务项目汇集
  10. JAVA Junit error java.lang.SecurityException: class junit.framework.JUnit4TestCaseFacade
  11. 本地tomcat启动war包_「shell脚本」懒人运维之自动升级tomcat应用(war包)
  12. regsvr32.exe
  13. android 内凹的圆角,css实现内凹圆角,利用圆角反向进行(转)
  14. java tomcat 日志_java – 访问Tomcat中的详细日志
  15. 南阳oj a+b问题
  16. loadrunner可用许可证
  17. python爬知识星球付费数据_python抓取知识星球精选帖,制作为pdf文件
  18. Python RPM包制作
  19. javaweb区分PC端和移动端
  20. [单位] 常用单位换算表大全

热门文章

  1. flink cdc 连接posgresql 数据库相关问题整理
  2. python多进程处理大量图片
  3. 最大心率值=220一年龄
  4. 图片质量估计-如何判定一个人脸是否为阴阳脸(第一弹:python版本)
  5. Word如何连续使用格式刷
  6. 前端命名规范(经常查阅)
  7. macOS Big Sur系统安装
  8. 企业付款到零钱「微信小程序别样发放红包」
  9. 错题本2020.5.2
  10. U盘硬盘插入有声音但是不显示盘符,无法打开文件,图标灰色