mirror of
https://github.com/chai2010/advanced-go-programming-book.git
synced 2025-05-23 20:02:22 +00:00
修改排版 ch1-06
This commit is contained in:
parent
2da13f2e69
commit
d29be037ff
@ -1,6 +1,6 @@
|
||||
# 1.6 常见的并发模式
|
||||
|
||||
Go语言最吸引人的地方是它内建的并发支持。Go语言并发体系的理论是C.A.R Hoare在1978年提出的CSP(Communicating Sequential Process,通讯顺序进程)。CSP有着精确的数学模型,并实际应用在了Hoare参与设计的T9000通用计算机上。从NewSqueak、Alef、Limbo到现在的Go语言,对于对CSP有着20多年实战经验的Rob Pike来说,他更关注的是将CSP应用在通用编程语言上产生的潜力。作为Go并发编程核心的CSP理论的核心概念只有一个:同步通信。关于同步通信的话题我们在前面一节已经讲过,本节我们将简单介绍下Go语言中常见的并发模式。
|
||||
Go 语言最吸引人的地方是它内建的并发支持。Go 语言并发体系的理论是 _C.A.R Hoare_ 在 1978 年提出的 CSP(Communicating Sequential Process,通讯顺序进程)。CSP 有着精确的数学模型,并实际应用在了 Hoare 参与设计的 T9000 通用计算机上。从 NewSqueak、Alef、Limbo 到现在的 Go 语言,对于对 CSP 有着 20 多年实战经验的 _Rob Pike_ 来说,他更关注的是将 CSP 应用在通用编程语言上产生的潜力。作为 Go 并发编程核心的 CSP 理论的核心概念只有一个:同步通信。关于同步通信的话题我们在前面一节已经讲过,本节我们将简单介绍下 Go 语言中常见的并发模式。
|
||||
|
||||
首先要明确一个概念:并发不是并行。并发更关注的是程序的设计层面,并发的程序完全是可以顺序执行的,只有在真正的多核 CPU 上才可能真正地同时运行。并行更关注的是程序的运行层面,并行一般是简单的大量重复,例如 GPU 中对图像处理都会有大量的并行运算。为更好的编写并发程序,从设计之初 Go 语言就注重如何在编程语言层级上设计一个简洁安全高效的抽象模型,让程序员专注于分解问题和组合方案,而且不用被线程管理和信号互斥这些繁琐的操作分散精力。
|
||||
|
||||
@ -356,7 +356,6 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
不过 `gatefs` 对此做一个抽象类型 `gate`,增加了 `enter` 和 `leave` 方法分别对应并发代码的进入和离开。当超出并发数目限制的时候,`enter` 方法会阻塞直到并发数降下来为止。
|
||||
|
||||
```go
|
||||
@ -368,7 +367,6 @@ func (g gate) leave() { <-g }
|
||||
|
||||
`gatefs` 包装的新的虚拟文件系统就是将需要控制并发的方法增加了 `enter` 和 `leave` 调用而已:
|
||||
|
||||
|
||||
```go
|
||||
type gatefs struct {
|
||||
fs vfs.FileSystem
|
||||
@ -384,7 +382,6 @@ func (fs gatefs) Lstat(p string) (os.FileInfo, error) {
|
||||
|
||||
我们不仅可以控制最大的并发数目,而且可以通过带缓存 Channel 的使用量和最大容量比例来判断程序运行的并发率。当管道为空的时候可以认为是空闲状态,当管道满了时任务是繁忙状态,这对于后台一些低级任务的运行是有参考价值的。
|
||||
|
||||
|
||||
## 1.6.5 赢者为王
|
||||
|
||||
采用并发编程的动机有很多:并发编程可以简化问题,比如一类问题对应一个处理线程会更简单;并发编程还可以提升性能,在一个多核 CPU 上开 2 个线程一般会比开 1 个线程快一些。其实对于提升性能而言,程序并不是简单地运行速度快就表示用户体验好的;很多时候程序能快速响应用户请求才是最重要的,当没有用户请求需要处理的时候才合适处理一些低优先级的后台任务。
|
||||
@ -413,15 +410,13 @@ func main() {
|
||||
|
||||
通过适当开启一些冗余的线程,尝试用不同途径去解决同样的问题,最终以赢者为王的方式提升了程序的相应性能。
|
||||
|
||||
|
||||
## 1.6.6 素数筛
|
||||
|
||||
在“Hello world 的革命”一节中,我们为了演示 Newsqueak 的并发特性,文中给出了并发版本素数筛的实现。并发版本的素数筛是一个经典的并发例子,通过它我们可以更深刻地理解 Go 语言的并发特性。“素数筛”的原理如图:
|
||||
|
||||

|
||||
|
||||
*图 1-13 素数筛*
|
||||
|
||||
_图 1-13 素数筛_
|
||||
|
||||
我们需要先生成最初的 `2, 3, 4, ...` 自然数序列(不包含开头的 0、1):
|
||||
|
||||
@ -478,7 +473,7 @@ func main() {
|
||||
|
||||
## 1.6.7 并发的安全退出
|
||||
|
||||
有时候我们需要通知goroutine停止它正在干的事情,特别是当它工作在错误的方向上的时候。Go语言并没有提供在一个直接终止Goroutine的方法,由于这样会导致goroutine之间的共享变量处在未定义的状态上。但是如果我们想要退出两个或者任意多个Goroutine怎么办呢?
|
||||
有时候我们需要通知 Goroutine 停止它正在干的事情,特别是当它工作在错误的方向上的时候。Go 语言并没有提供在一个直接终止 Goroutine 的方法,由于这样会导致 Goroutine 之间的共享变量处在未定义的状态上。但是如果我们想要退出两个或者任意多个 Goroutine 怎么办呢?
|
||||
|
||||
Go 语言中不同 Goroutine 之间主要依靠管道进行通信和同步。要同时处理多个管道的发送或接收操作,我们需要使用 `select` 关键字(这个关键字和网络编程中的 `select` 函数的行为类似)。当 `select` 有多个分支时,会随机选择一个可用的管道分支,如果没有可用的管道分支则选择 `default` 分支,否则会一直保存阻塞状态。
|
||||
|
||||
@ -617,7 +612,6 @@ func main() {
|
||||
|
||||
现在每个工作者并发体的创建、运行、暂停和退出都是在 `main` 函数的安全控制之下了。
|
||||
|
||||
|
||||
## 1.6.8 context 包
|
||||
|
||||
在 Go1.7 发布时,标准库增加了一个 `context` 包,用来简化对于处理单个请求的多个 Goroutine 之间与请求域的数据、超时和退出等操作,官方有博文对此做了专门介绍。我们可以用 `context` 包来重新实现前面的线程安全退出或超时的控制:
|
||||
@ -771,7 +765,7 @@ func main() {
|
||||
```
|
||||
|
||||
执行上面这个例子很容易就复现了死锁的问题,原因是素数筛中的 `ctx.Done()` 位于 `if i := <-in; i%prime != 0` 判断之内,
|
||||
而这个判断可能会一直阻塞,导致goroutine无法正常退出。让我们来解决这个问题。
|
||||
而这个判断可能会一直阻塞,导致 Goroutine 无法正常退出。让我们来解决这个问题。
|
||||
|
||||
```go
|
||||
package main
|
||||
@ -907,6 +901,7 @@ func main() {
|
||||
```
|
||||
|
||||
在上面这个例子中主要有以下几点需要关注:
|
||||
|
||||
1. 通过 `for range` 循环保证了输入管道被关闭时,循环能退出,不会出现死循环;
|
||||
2. 通过 `defer close` 保证了无论是输入管道被关闭,还是 ctx 被取消,只要素数筛退出,都会关闭输出管道。
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user