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.















