@@ -1,4 +1,4 @@
# 14.1 并发, 并行和协程
# 14.1 并发、 并行和协程
## 14.1.1 什么是协程
@@ -6,18 +6,17 @@
并行是一种通过使用多处理器以提高速度的能力。所以并发程序可以是并行的,也可以不是。
公认的,使用多线程的应用难以做到准确,最主要的问题是内存中的数据共享,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果(称作 `竞态` )。
公认的,使用多线程的应用难以做到准确,最主要的问题是内存中的数据共享,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果(称作`竞态` )
**不要使用全局变量或者共享内存,它们会给你的代码在并发运算的时候带来危险。**
!!不要使用全局变量或者共享内存,它们会给你的代码在并发运算的时候带来危险 。
解决之道在于同步不同的线程,对数据加锁,这样同时就只有一个线程可以变更数据。在 Go 的标准库 `sync` 中有一些工具用来在低级别的代码中实现加锁;我们在第 [9.3 ](9.3.md ) 节中讨论过这个问题。不过过去的软件开发经验告诉我们这会带来更高的复杂度,更容易使代码出错以及更低的性能,所以这个经典的方法明显不再适合现代多核/多处理器编程:`thread-per-connection` 模型不够有效 。
解决之道在于同步不同的线程, 对数据加锁, 这样同时就只有一个线程可以变更数据。在Go的标准库`sync` 中有一些工具用来在低级别的代码中实现加锁;我们在章节[9.3 ](9.3.md )中讨论过这个问题。不过过去的软件开发经验告诉我们这会带来更高的复杂度,更容易使代码出错以及更低的性能,所以这个经典的方法明显不再适合现代多核/多处理器编程:"`thread-per-connection` "模型不够有效 。
Go更倾向于其他的方式, 在诸多比较合适的范式中, 有个被称作`Communicating Sequential Processes( 顺序通信处理) ` (CSP, C. Hoare发明的)还有一个叫做`message passing-model( 消息传递) ` ( 已经运用在了其他语言中, 比如Eralng) 。
Go 更倾向于其他的方式,在诸多比较合适的范式中,有个被称作 `Communicating Sequential Processes( 顺序通信处理) ` ( CSP, C. Hoare 发明的)还有一个叫做 `message passing-model( 消息传递) ` (已经运用在了其他语言中,比如 Eralng) 。
在 Go 中,应用程序并发处理的部分被称作 `goroutines( 协程) ` ,它可以进行更有效的并发运算。在协程和操作系统线程之间并无一对一的关系:协程是根据一个或多个线程的可用性,映射(多路复用,执行于)在他们之上的;协程调度器在 Go 运行时很好的完成了这个工作。
协程工作在相同的地址空间中,所以共享内存的方式一定是同步的;这个可以使用`sync` 包来实现(参见章节 [9.3 ](9.3.md )) , 不过我们很不鼓励这样做: Go使用`channels` 来同步协程(可以参见[14.2 ](14.2.md )等章节)
协程工作在相同的地址空间中,所以共享内存的方式一定是同步的;这个可以使用 `sync` 包来实现(参见第 [9.3 ](9.3.md ) 节) , 不过我们很不鼓励这样做: Go 使用 `channels` 来同步协程(可以参见第 [14.2 ](14.2.md ) 节 等章节)
当系统调用(比如等待 I/O) 阻塞协程时, 其他协程会继续在其他线程上工作。协程的设计隐藏了许多线程创建和管理方面的复杂工作。
@@ -25,9 +24,9 @@ Go更倾向于其他的方式, 在诸多比较合适的范式中, 有个被称
协程可以运行在多个操作系统线程之间,也可以运行在线程之内,让你可以很小的内存占用就可以处理大量的任务。由于操作系统线程上的协程时间片,你可以使用少量的操作系统线程就能拥有任意多个提供服务的协程,而且 Go 运行时可以聪明的意识到哪些协程被阻塞了,暂时搁置它们并处理其他协程。
存在两种并发方式:确定性的(明确定义排序)和非确定性的(加锁/互斥从而未定义排序) 。Go的协程和通道理所当然的支持确定性的并发方式( 例如通道具有一个sender和一个receiver) 。我们会在章节 [14.7 ](14.7.md )中使用一个常见的算法问题(工人问题)来对比两种处理方式。
存在两种并发方式:确定性的(明确定义排序)和非确定性的(加锁/互斥从而未定义排序) 。Go 的协程和通道理所当然的支持确定性的并发方式(例如通道具有一个 sender 和一个 receiver) 。我们会在第 [14.7 ](14.7.md ) 节 中使用一个常见的算法问题(工人问题)来对比两种处理方式。
协程是通过使用关键字`go` 调用( 执行) 一个函数或者方法来实现的( 也可以是匿名或者lambda函数) 。这样会在当前的计算过程中开始一个同时进行的函数, 在相同的地址空间中并且分配了独立的栈, 比如: `go sum(bigArray)` // 在后台计算总和
协程是通过使用关键字 `go` 调用(执行)一个函数或者方法来实现的(也可以是匿名或者 lambda 函数)。这样会在当前的计算过程中开始一个同时进行的函数,在相同的地址空间中并且分配了独立的栈,比如:`go sum(bigArray)` , 在后台计算总和。
协程的栈会根据需要进行伸缩,不出现栈溢出;开发者不需要关心栈的大小。当协程结束的时候,它会静默退出:用来启动这个协程的函数不会得到任何的返回值。
@@ -49,11 +48,11 @@ Go的并发原语提供了良好的并发设计基础: 表达程序结构以便
## 14.1.3 使用 GOMAXPROCS
在gc编译器下( 6g或者8g) 你必须设置GOMAXPROCS为一个大于默认值1 的数值来允许运行时支持使用多于1 个的操作系统线程, 所有的协程都会共享同一个线程除非将GOMAXPROCS设置为一个大于1 的数。当GOMAXPROCS大于1 时,会有一个线程池管理许多的线程。通过`gccgo` 编译器GOMAXPROCS有效的与运行中的协程数量相等。假设n 是机器上处理器或者核心的数量。如果你设置环境变量GOMAXPROCS>=n, 或者执行`runtime.GOMAXPROCS(n)` ,接下来协程会被分割(分散)到n 个处理器上。更多的处理器并不意味着性能的线性提升。有这样一个经验法则,对于n 个核心的情况设置GOMAXPROCS为 n-1以获得最佳性能, 也同样需要遵守这条规则: 协程的数量 > 1 + GOMAXPROCS > 1
在 gc 编译器下( 6g 或者 8g) 你必须设置 GOMAXPROCS 为一个大于默认值 1 的数值来允许运行时支持使用多于 1 个的操作系统线程,所有的协程都会共享同一个线程除非将 GOMAXPROCS 设置为一个大于 1 的数。当 GOMAXPROCS 大于 1 时,会有一个线程池管理许多的线程。通过 `gccgo` 编译器 GOMAXPROCS 有效的与运行中的协程数量相等。假设 n 是机器上处理器或者核心的数量。如果你设置环境变量 GOMAXPROCS>=n, 或者执行 `runtime.GOMAXPROCS(n)` ,接下来协程会被分割(分散)到 n 个处理器上。更多的处理器并不意味着性能的线性提升。有这样一个经验法则,对于 n 个核心的情况设置 GOMAXPROCS 为 n-1 以获得最佳性能,也同样需要遵守这条规则:协程的数量 > 1 + GOMAXPROCS > 1。
所以如果在某一时间只有一个协程在执行,不要设置 GOMAXPROCS!
还有一些通过实验观察到的现象:在一台1颗 CPU的笔记本电脑上, 增加GOMAXPROCS到9 会带来性能提升。在一台32核的机器上, 设置GOMAXPROCS=8会达到最好的性能, 在测试环境中, 更高的数值无法提升性能。如果设置一个很大的GOMAXPROCS只会带来轻微的性能下降; 设置GOMAXPROCS=100, 使用“top”命令和“H” 选项查看到只有7 个活动的线程。
还有一些通过实验观察到的现象:在一台 1 颗 CPU 的笔记本电脑上,增加 GOMAXPROCS 到 9 会带来性能提升。在一台 32 核的机器上,设置 GOMAXPROCS=8 会达到最好的性能,在测试环境中,更高的数值无法提升性能。如果设置一个很大的 GOMAXPROCS 只会带来轻微的性能下降;设置 GOMAXPROCS=100, 使用 `top` 命令和 `H` 选项查看到只有 7 个活动的线程。
增加 GOMAXPROCS 的数值对程序进行并发计算是有好处的;
@@ -64,6 +63,7 @@ Go的并发原语提供了良好的并发设计基础: 表达程序结构以便
## 14.1.4 如何用命令行指定使用的核心数量
使用 `flags` 包,如下:
```go
var numCores = flag . Int ( "n" , 2 , "number of CPU cores to use" )
@@ -75,6 +75,7 @@ runtime.GOMAXPROCS(*numCores)
协程可以通过调用`runtime.Goexit()` 来停止,尽管这样做几乎没有必要。
示例 14.1-[goroutine1.go ](examples/chapter_14/goroutine1.go ) 介绍了概念:
```go
package main
@@ -105,7 +106,9 @@ func shortWait() {
fmt . Println ( "End of shortWait()" )
}
```
输出
输出:
```
In main()
About to sleep in main()
@@ -115,6 +118,7 @@ End of shortWait()
End of longWait()
At the end of main() // after 10s
```
`main()` , `longWait()` 和 `shortWait()` 三个函数作为独立的处理单元按顺序启动,然后开始并行运行。每一个函数都在运行的开始和结束阶段输出了消息。为了模拟他们运算的时间消耗,我们使用了 `time` 包中的 `Sleep` 函数。`Sleep()` 可以按照指定的时间来暂停函数或协程的执行, 这里使用了纳秒( ns, 符号 1e9 表示 1 乘 10 的 9 次方, e=指数)。
他们按照我们期望的顺序打印出了消息,几乎都一样,可是我们明白这是模拟出来的,以并行的方式。我们让 `main()` 函数暂停 10 秒从而确定它会在另外两个协程之后结束。如果不这样(如果我们让 `main()` 函数停止 4 秒),`main()` 会提前结束,`longWait()` 则无法完成。如果我们不在 `main()` 中等待,协程会随着程序的结束而消亡。
@@ -126,6 +130,7 @@ At the end of main() // after 10s
为了对比使用一个线程,连续调用的情况,移除 go 关键字,重新运行程序。
现在输出:
```
In main()
Beginning longWait()
@@ -135,19 +140,21 @@ End of shortWait()
About to sleep in main()
At the end of main() // after 17 s
```
协程更有用的一个例子应该是在一个非常长的数组中查找一个元素。
将数组分割为若干个不重复的切片,然后给每一个切片启动一个协程进行查找计算。这样许多并行的线程可以用来进行查找任务,整体的查找时间会缩短(除以协程的数量)。
## 14.1.5 Go 协程( goroutines) 和协程( coroutines)
( 译者注: 标题中的“Go协程( goroutines) ”即是14章讲的协程指的是go 语言中的协程。而“协程( coroutines) ”指的是其他语言中的协程概念, 仅在本节出现。)
( 译者注: 标题中的“Go协程( goroutines) ” 即是 14 章讲的协程指的是 Go 语言中的协程。而“协程( coroutines) ”指的是其他语言中的协程概念, 仅在本节出现。)
在其他语言中,比如 C#, Lua 或者 Python 都有协程的概念。这个名字表明它和 G o协程有些相似, 不过有两点不同:
* go协程意味着并行( 或者可以以并行的方式部署) , 协程一般来说不是这样的
* go协程通过通道来通信; 协程通过让出和恢复操作来通信
Go协程比协程更强大, 也很容易从协程的逻辑复用到go协程。
- Go 协程意味着并行(或者可以以并行的方式部署),协程一般来说不是这样的
- Go 协程通过通道来通信;协程通过让出和恢复操作来通信
Go 协程比协程更强大,也很容易从协程的逻辑复用到 Go 协程。
## 链接