惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Microsoft Azure Blog
Microsoft Azure Blog
V
V2EX
博客园 - 【当耐特】
WordPress大学
WordPress大学
爱范儿
爱范儿
美团技术团队
宝玉的分享
宝玉的分享
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
小众软件
小众软件
量子位
Hugging Face - Blog
Hugging Face - Blog
B
Blog RSS Feed
Recorded Future
Recorded Future
Engineering at Meta
Engineering at Meta
雷峰网
雷峰网
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
M
MIT News - Artificial intelligence
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 聂微东
H
Hackread – Cybersecurity News, Data Breaches, AI and More
腾讯CDC
大猫的无限游戏
大猫的无限游戏
Jina AI
Jina AI
博客园 - 叶小钗
GbyAI
GbyAI
Y
Y Combinator Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
F
Full Disclosure
G
Google Developers Blog
D
Docker
T
Tailwind CSS Blog
C
Check Point Blog
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
T
The Blog of Author Tim Ferriss
B
Blog
博客园 - 三生石上(FineUI控件)
博客园 - Franky
H
Help Net Security
MyScale Blog
MyScale Blog
U
Unit 42
D
DataBreaches.Net
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
I
InfoQ
阮一峰的网络日志
阮一峰的网络日志
The GitHub Blog
The GitHub Blog
L
LangChain Blog
有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
Microsoft Security Blog
Microsoft Security Blog

陈少文的网站

巨变与机遇的未来十年 Kubernetes 平台管理软件压力测试方案 使用镜像部署 Hexo 静态页面 终于等到你 - GitHub 镜像仓库服务(ghcr.io) 一起来学 Go --(6)Interface 什么是函数式编程 如何在 Kubernetes 集群集成 Kata 柯里化与偏函数 使用 PyGithub 自动创建 Label 软件产品是团队能力的输出 Helm 2 、Helm 3 比较 IoT 变现 Kubernetes 中的 DNS 服务 国内的 Helm 镜像源 Harbor 使用自签证书支持 Https 访问 DevOps 工具链之 Prow 如何使用 kfctl 安装 Kubeflow VS Code 无法下载 Go 插件的工具包 工程师更应具有服务精神 你不知道的 Docker 使用技巧 使用 Docker 运行 Tensorflow 论中国 什么是左移 如何清空 Git 仓库全部历史记录 一禅小和尚 有风吹过厨房 时间的玫瑰 如何在 CentOS 安装 GPU 驱动 开发 Tips(19) 使用 Velero 备份 Kubernetes 集群 Kubernetes Cheat Sheet 开发 Tips(18) 如何构建一个 Java 工程 开发 Tips(17) KubeSpray 安装 Kubernetes 报错 ip in ansible_all_ipv4_addresses 基于 Kubernetes 和 Jenkins 搭建自动化测试系统 在 Kubernetes 上动态创建 Jenkins Slave 使用 Jenkins 进行服务拨测 开发 Tips(16) Kubernetes 签发 Ingress 证书及日常故障运维 Kubernetes 中 Deployment 的基本操作 Kubernetes 中的证书 如何使用 KubeBuilder 开发一个 Operator Kubernetes 1.6.0 安装问题汇总 镜像管理工具 -- Harbor 开发 Tips(15) Docker 如何拉取镜像 开发 Tips(14) 使用 Helm 安装 harbor 开发 Tips(13) 使用 S2I 构建云原生应用 在 Kubernetes 中使用 emptyDir、hostPath、localVolume 开发 Tips(12) 开发 Tips(11) 代码质量分析工具 SonarQube 使用 Kubeadm 安装 Kubernetes 集群 一起来学 Go --(4)常用函数 Kubernetes 中的 Ceph Kubernetes 之 Volumes Kubernetes 之 Labels、Selectors 开发 Tips(10) 开源正在重构商业模式 Kubernetes 之网络 Kubernetes 之 API 使用 Helm 和 Operator 快速部署 Prometheus Kubernetes 复杂有状态应用管理框架 -- Operator Kubernetes 的包管理器 -- Helm 一起来学 Go --(3)Go Modules 如何一步一步地优化博客方案 kubectl 实用指南 Kubernetes 中的基本概念 搭建远程 Kubernetes 开发环境 大公司和小公司的 ToB 思路 开发 Tips(9) Go 入门指南 一起来学 Go --(2)数据与逻辑结构 如何预防 Web 富文本中的 XSS 攻击 django-xss-cleaner 云工作时代 一起来学 Go --(1)背景与特点 SaaS 开发团队的不同阶段 你不知道的 Git 使用技巧 输出既服务 微服务设计 继续奔跑 开发 Tips(8) 从账户安全到二次验证 Django 性能之数据库查询优化 Django 性能之分库分表 敏捷开发之研发流程 打造一致性的团队 开发 Tips(7) Pytest 进阶学习之 Mock PaaS 部署之 buildpack Go 开发配置 领域输出才是 PaaS 的核心竞争力 Pytest 入门学习 开发 Tips(6) 如何使用 Jenkins、Docker、GitLab 搭建 Django 自动化部署流程 开发 Tips(5)
一起来学 Go --(5)Goroutine 和 Channel
微信公众号 · 2020-09-03 · via 陈少文的网站

1. Go 中的并发模型

1.1 通信模型 CSP

CSP 全称 Communicating Sequential Process ,通信顺序进程,描述的是一种并发通信模型。Process 可以使用很多个 Channel ,而 Channel 不关心谁在使用它,只负责收发数据。

Go 社区中,有一句非常著名的论断: 不要通过共享内存来通信,要通过通信来共享内存。意思是,不要在 Process 之间传递指针,而应该封装成对象,丢到 Channel 中,等待 Process 的消费。

CSP 中的 Process/Channel 对应 Go 语言中的 Goroutine/Channel ,是 Go 并发编程的基石。Goroutine 用于执行任务,Channel 用于 Goroutine 任务之间的通信。

1.2 两级线程模型

用户线程只是用户程序中的一堆数据,内核线程才是系统中的实际线程。用户线程得到调度时,内核线程读取用户线程数据进行执行。

因此,有必要了解一下用户线程和内核线程的关系,也就是线程模型。根据两者映射关系,可以分为一对一、多对一、多对多三种调度模型。(一个用户线程绑定到多个内核线程的模型,目前没有实际案例。)

一对一,一个用户线程绑定一个内核线程。借助于内核的调度,可以很方便地实现并发,但是内核线程频繁切换,调度成本很高。多对一,多个用户线程绑定一个内核线程。通过程序逻辑,控制多个线程的调度,但是只绑定了一个内核线程,只是宏观上的并发,不是真正的并行。多对多,多个用户线程绑定多个内核线程。这样可以充分利用多核的运算性能。

两级线程模型将用户调度和内核调度分开,用户调度只需要关注用户线程与逻辑处理器的调度,内核调度只需要关注逻辑处理器和物理处理器的调度。

1.3 调度模型 G-P-M

Go 能充分利用 CPU 多核性能,很重要的一点就是基于多对多线程模型,实现了 G-P-M 调度模型。有些语言的并发性能,依赖于第三方库,在第三方库中实现了用户线程、协程的调度。但在 Go 语言中,这种调度的能力直接作为语言特性被提供。下图是 Go 调度器的模型:

先来看下相关的概念:

  • G ,Goroutine

每个 Goroutine 对应一个 G 结构体,用于存储 G 运行堆栈、状态,也就是一个执行逻辑。

  • P,Processor

逻辑处理器,G 需要绑定到 P 才能被调度,P 向 M 提供内存分配状态、任务队列等上下文环境。

  • M,Machine

物理处理器,P 需要绑定到 M 才能被调度。

调度的过程是这样的,G 创建后全部进入 Global 队列,等待调度。P 找到空闲的 M ,绑定之后,开始执行 Local 队列中的 G 。如果 G 进行系统调用,导致 M 处于阻塞状态,那么 P 将带着 Local 队列进行漂移到其他 M。当 P 的 Local 队列中没有 G 时,会依次从 Global 队列、其他 P 的 Local 队列中获取 G,直到全部 G 执行完成。

一个 Goruntine 初始内存只要 2KB ,因此只需要很少的资源就可以达到很高的并发量。但其占用的内存可以不断地增长,在 64 位机器上可达 1GB。这样,既保证了启动速度、数量,又兼顾了某些大内存消耗的场景。

2. 代码中的 Goroutine 和 Channel

Goroutine 是实际并发执行的实体。借助 Channel 通信,Go 实现了 Goroutine 之间传递的数据和同步。

2.1 Goroutine

Goroutine 是用协程实现的。什么是协程?协程是一个轻量级的线程,一个执行逻辑。但是这个执行逻辑不是由 OS 调度,在 Go 语言中由 Goroutine 的调度器来管理和调度这些协程。

G-P-M 模型就是 Goroutine 的调度器的实现模型。下面我们直接看个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"fmt"
	"time"
)

func main() {

	func(msg string) {
		fmt.Println(msg)
	}("1/3 - direct")

	go func(msg string) {
		fmt.Println(msg)
	}("2/3 - goroutine")

	time.Sleep(time.Second)
	fmt.Println("3/3 - done")
}

通过 go 关键字,就可以很方便的将函数以非阻塞的形式并发执行。如果没有 time.Sleep ,主程序将不会等待打印 2/3 - goroutine ,而直接退出。

2.2 Channel 的生命周期

  • 创建

Channel 需要使用 make 进行创建,Channel 的零值为 nil。

1
messages := make(chan string)
  • 写数据
1
2
m := "This is a message"
messages <- m
  • 读数据
  • 关闭 Channel

在读写数据时,有点类似 Linux 系统中的管道操作。

2.3 Channel 分类

根据数据流方向,可以将 Channel 分为三种:

  • 声明 T 类型的双向通道

通常,使用的都是双向通道。

  • 声明只能发送 T 类型的通道
  • 声明只能接受 T 类型的通道

根据是否带缓冲区域,Channel 又分为两种:

  • 不带缓冲,可以看作是同步模式,默认采用这种模式

发送和接收同时发生,当有一方没有就绪时,另一方会处于等待状态。

  • 带缓冲,可以看作是异步模式

缓冲区未满时,发送和接收是异步的,当缓冲区满时,发送才会阻塞,等待有数据被消费。

在使用完 Channel 之后,需要关闭 Channel 。如果继续发送数据,则会引发 Panic 。但可以继续读取 Channel 中的数据,没有缓冲区的 Channel 返回的是零值,有缓冲区的 Channel 接收完数据之后,也是零值。这保证了,即使 Channel 关闭,Channel 中的数据不会丢失,依然能够得到可靠的逻辑处理。

3. 通信示例

  • Channel 通信

在 G-P-M 的示例中,通过 time.Sleep 等待 Goroutine 的完成。但是 Goroutine 的完成时间是不确定的,可能几十毫秒、也有可能几十秒,这种控制方式是不可靠的。

无缓存的 Channel 可以用于这类通信场景。下面是一个相关的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func main() {

	messages := make(chan string)

	go func() { messages <- "ping" }()

	msg := <-messages
	fmt.Println(msg)
}

如果 Goroutine 不涉及数据的传输,这里也可以声明一个布尔类型的 Channel , done := make(chan bool) 。当 Goroutine 完成全部执行逻辑之后,往 done 中写入一个布尔类型即可。

  • WaitGroup 控制 Goroutine

Go 的优势是高并发,意味着可能同时具有很多个 Goroutine 在执行。那么如果控制多个 Goroutine 的并发行为呢?答案就是 WaitGroup 。一起看下面的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int, wg *sync.WaitGroup) {

	defer wg.Done()

	fmt.Printf("Worker %d starting\n", id)

	time.Sleep(time.Second)
	fmt.Printf("Worker %d done\n", id)
}

func main() {

	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
		wg.Add(1)
		go worker(i, &wg)
	}

	wg.Wait()
}

这段代码每次执行的结果都不一样,因为并发的同等优先级的Goroutine 执行顺序无法控制。

WaitGroup 是通过计数器和信号量实现并发控制的。wg.Add 时,计数器 + 1 ;wg.Done() 时,计数器 -1

4. 参考