Go - CSP
并发 vs 并行
单核
单进程
内部仅有一条代码执行流,不存在
竞态
,无需考虑同步
问题
- 每个
单进程应用
对应一个操作系统进程
- 操作系统的多个进程按照
时间片
大小,被轮流调度
到单核
上执行 - 单核在
某个时刻
只能执行一个进程对应的程序代码,两个进程不存在并行
执行的可能并行
(Parallelism):在同一时刻
,有两个或两个以上
的进程的代码在处理器上执行- 因此,
多处理器
或者多核处理器
是并行执行的必要条件
多进程
应用结构清晰,维护性更好
- 应用通过
fork
等系统调用创建多个子进程,共同实现应用的功能 - App1 内部划分为多个模块,每个模块用一个
进程
来承载执行,每个模块都是一个单独的执行流
- 在单核下,多进程依然无法并行执行,只能按照
时间片
被操作系统调度到单核上执行
多核
多线程
- 进程并不适合用于承载采用了
并发设计
的应用的模块执行流- 进程是操作系统中
资源分配
的基本单位:应用代码+应用数据、操作系统资源(文件描述符、内存地址空间等) - 因此,进程的创建、切换和撤销的
代价
都很大
- 进程是操作系统中
- 线程是运行于
进程上下文
中的更轻量
的执行流
- 随着处理器技术的发展,
多核处理器
成为了主流,让真正的并行
成为了可能 - 基于线程的应用通常采用
单进程多线程
的模型一个应用
对应一个进程
,应用通过并发设计
将自身划分为多个模块
,每个模块由一个线程
独立承载执行- 多个线程
共享
该进程内的资源,而线程作为执行单元
可以被独立调度
到处理器
上执行- 线程的创建、切换和撤销的代价相对于进程要
小得多
- 线程的创建、切换和撤销的代价相对于进程要
- 当应用的多个线程
同时
被调度到不同
的处理器核上执行时,该应用是并行
的
对比
并发
关乎结构
、而并行
关乎执行
- 并发是应用
设计
和实现
阶段要考虑的问题- 考虑如何将应用
划分
为多个相互配合、可独立执行的模块
- 采用
并发设计
的程序并不一定是并行执行
的,如仅有一个单核 CPU
- 考虑如何将应用
- 并发让并行
变得容易
- 采用
并发设计
的应用可以将负载自然扩展到各个 CPU 核上,从而提升处理器的利用效率
- 采用
并发
是一种能力
,让程序可以由若干代码片段组合
而成,并且每个片段都是独立运行
的
并行
的必要条件
是具有多个处理器
或者多核处理器
原生线程
- 在传统编程语言(C++、Java)中,基于
多线程模型
的应用设计,是一种典型的并发设计
- 但并非
面向并发
而生,多以操作系统线程
作为承载分解后的代码片段的执行单元
,由操作系统执行调度
不足
复杂
- 在 C++ 中,线程
创建容易退出难
- 并发执行单元间的
通信困难且易错
- 一旦涉及到
共享内存
,会涉及到各种锁互斥
机制,容易造成死锁
- 一旦涉及到
- 需要设定
线程栈大小
- 在 C++ 中,线程
难以规模化
- 线程的使用代价虽然远小于进程,但依然
不能创建大量线程
- 每个线程会
占用不小的资源
,且操作系统调度切换
线程的代价也不小
- 每个线程会
- 对于
网络服务
而言,只能基于少量线程做IO 多路复用
- 线程的使用代价虽然远小于进程,但依然
goroutine
goroutine
- 由Go Runtime
负责调度的轻量级用户线程
,为并发设计
提供了原生支持
优势
占用资源少 + 在用户层调度 + 支持 CSP 模型
- 占用资源小,每个 goroutine 的
初始栈大小
仅为2K
- 由
Go Runtime
而非操作系统调度
,goroutine 上下文切换在用户层
完成,开销更小 - 在
语言层面
而不是通过标准库提供- goroutine 由 go 关键字创建,
一退出
就会被回收
或者销毁
,开发体验更佳
- goroutine 由 go 关键字创建,
- 语言内置
channel
作为 goroutine 间的通信原语
,为并发设计提供了强大支撑
用法
无论是 Go Runtime 代码,还是用户层代码,都运行在 goroutine 中
1 | // 基于已有的具名函数 |
1 | // 基于已有的具名方法 |
- 一个应用内部启动的所有 goroutine
共享
进程空间的资源 - 如果多个 goroutine 访问
同一块
内存数据,将会存在竞争
,此时需要进行 goroutine 之间的同步
退出 goroutine
- goroutine 的
使用代价很低
,goroutine 的执行函数返回
,意味着 goroutine 退出 - 如果
main goroutine
退出,意味着整个应用
退出
goroutine 执行的
函数
或者方法
即便有返回值
,Go 也会忽略
这些返回值
通信
传统
- 传统的编程语言(C++、Java 等)并非
面向并发
而生,所以面向并发的逻辑多是基于操作系统线程
- 并发的
执行单元
(线程)之间的通信,利用的也是操作系统
提供的线程
或者进程
间通信的原语
- 如:共享内存、信号、管道、消息队列、套接字等
- 使用最广泛的是结合了
线程同步原语
(锁
以及更为低级的原子
操作)的共享内存
方式
- 并发的
- 传统编程语言的
并发模型
是基于共享内存
的 -难用
+易错
CSP
Communicating sequential processes,CSP 模型旨在
简化并发程序的编写
- 输入输出为基本的编程原语,数据处理逻辑(
P
)调用输入原语获取数据,顺序处理数据,并将结果输出到输出原语 - 一个符合 CSP 模型的并发程序应该是一组通过
输入输出
原语连接
起来的P 集合
- CSP 的
组合
思想与 Go 的设计哲学不谋而合
- CSP 的
sequential processes
P 为一个抽象概念,代表任何顺序处理逻辑
的封装,获得输入数据,并生产可以被其它 P 消费的输出数据
- P 不一定就是操作系统的进程或者线程,在 Go 中,
P
就是goroutine
- 为了实现 CSP 并发模型中的
输入输出原语
,Go 引入了 goroutine 之间的通信原语channel
- 通过 channel 将 goroutine
组合
连接在一起
1 | func spawn(f func() error) <-chan error { |
CSP
为 Go 支持的主流
并发模型 -推荐
- Go 也支持
基于共享内存
的并发模型- 提供基本的
低级别同步原语
,如 sync 包中互斥锁
、条件变量
、读写锁
、原子操作
等
- 提供基本的
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.