1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-23 20:02:22 +00:00

fix typo & match coding standard

This commit is contained in:
lewgun 2018-01-03 13:37:59 +08:00 committed by GitHub
parent 10dfb38bbb
commit 6acaa94770
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,20 +1,20 @@
# 1.5. 面向并发的内存模型
在早期CPU都是以单核的形式顺序执行机器指令。而作为Go语言祖先C语言正是这种顺序编程语言的代表。顺序编程语言中的顺序指的是,所有的指令都是以串行的方式执行在相同的时刻有且仅有一个CPU在顺序执行程序的指令。
在早期CPU都是以单核的形式顺序执行机器指令。Go语言祖先C语言正是这种顺序编程语言的代表。顺序编程语言中的顺序是指:所有的指令都是以串行的方式执行在相同的时刻有且仅有一个CPU在顺序执行程序的指令。
不过随着处理器的发展单核时代以提升处理器频率的方式遇到的瓶颈目前各种主流的CPU频率基本被锁定在了3GHZ附近。单核CPU的发展虽然停滞了但是却给多核CPU的发展带来了机遇。相应地编程语言也开始逐步向并行化的方向发展。Go语言正是在多核和网络化的时代背景下诞生的原生支持并发的编程语言。
随着处理器技术的发展单核时代以提升处理器频率来提高运行效率的方式遇到了瓶颈目前各种主流的CPU频率基本被锁定在了3GHZ附近。单核CPU的发展的停滞给多核CPU的发展带来了机遇。相应地编程语言也开始逐步向并行化的方向发展。Go语言正是在多核和网络化的时代背景下诞生的原生支持并发的编程语言。
常见的并行编程有多种模型,主要有多线程、消息传递等。理论上讲,多线程和基于消息的并发编程是等价的。但是多线程并发模型可以自然对应到多核的处理器,主流的操作系统也都提供了系统级的多线程支持,而且从概念上讲多线程似乎也更直观,因此多线程编程模型逐步被吸纳到主流的编程语言特性或语言扩展库中。而主流编程语言对基于消息的并发编程模型支持比较少Erlang语言是支持基于消息传递并发编程模型的代表者不过Erlang编程语言的并发体之间是不共享内存的。Go语言是基于消息并发模型的集大成者它将基于CSP模型的并发编程内置到了语言中通过一个go关键字就可以轻易地启动一个Goroutine同时Go语言中的Goroutine之间是共享内存的。
常见的并行编程有多种模型,主要有多线程、消息传递等。从理论上来看,多线程和基于消息的并发编程是等价的。由于多线程并发模型可以自然对应到多核的处理器,主流的操作系统因此也都提供了系统级的多线程支持,同时从概念上讲多线程似乎也更直观,因此多线程编程模型逐步被吸纳到主流的编程语言特性或语言扩展库中。而主流编程语言对基于消息的并发编程模型支持则相比较少Erlang语言是支持基于消息传递并发编程模型的代表者它的并发体之间不共享内存。Go语言是基于消息并发模型的集大成者它将基于CSP模型的并发编程内置到了语言中通过一个go关键字就可以轻易地启动一个Goroutine与Erlang不同的是Go语言的Goroutine之间是共享内存的。
## Goroutine和系统线程
Goroutine是Go语言特有的并发体是一种轻量级的线程由go关键字启动。在真实的Go语言的实现中goroutine和系统线程也不是等价的。尽管两者的区别实际上只是一个量的区别但正是这个量变引发Go语言并发编程质的飞跃。
Goroutine是Go语言特有的并发体是一种轻量级的线程由go关键字启动。在真实的Go语言的实现中goroutine和系统线程也不是等价的。尽管两者的区别实际上只是一个量的区别但正是这个量变引发Go语言并发编程质的飞跃。
首先每个系统级线程都会有一个固定大小的栈一般默认可能是2MB这个栈主要用来解决函数递归调用时函数参数和局部变量。因为是固定栈大小,导致很多只需要很小的栈空间的线程来说是一个巨大的浪费,同时对于少数但是需要巨大栈空间的线程来说又面临栈溢出的风险。通过降低固定的栈大小虽然可以提升空间的利用率允许创建更多的线程,或者增大栈的深度以允许更深的函数递归调用,不过这两者是没法同时兼得的。相反一个Goroutine会以一个很小的栈启动可能是2KB或4KB当遇到深度递归导致当前栈空间不足时Goroutine栈的大小会根据需要动态地伸缩主流实现中栈的最大值可达到1GB。因为启动的代价很小我们可以轻易地启动成千上万个Goroutine。
首先每个系统级线程都会有一个固定大小的栈一般默认可能是2MB这个栈主要用来保存函数递归调用时参数和局部变量。固定了栈的大小这导致了两个问题:一是对于很多只需要很小的栈空间的线程来说是一个巨大的浪费,二是对于少数需要巨大栈空间的线程来说又面临栈溢出的风险。针对这两个问题的解决方案是:要么降低固定的栈大小,提升空间的利用率;要么增大栈的深度以允许更深的函数递归调用,但这两者是没法同时兼得的。相反一个Goroutine会以一个很小的栈启动可能是2KB或4KB当遇到深度递归导致当前栈空间不足时Goroutine会根据需要动态地伸缩栈的大小主流实现中栈的最大值可达到1GB。因为启动的代价很小所以我们可以轻易地启动成千上万个Goroutine。
Go的运行时还包含了其自己的调度器这个调度器使用了一些技术手段可以在n个操作系统线程上多工调度m个goroutine。Go调度器的工作和内核的调度是相似的但是这个调度器只关注单独的Go程序中的goroutine。Goroutine采用的是半抢占式的协作调度只有但当前Goroutine发生阻塞时才会导致调度同时发生在用户态调度器会根据具体函数只保存必要的寄存器切换的代价要比系统线程低得多。运行时有一个`runtime.GOMAXPROCS`变量,用于控制当前运行运行正常非阻塞Goroutine的系统线程数目。
Go的运行时还包含了其自己的调度器这个调度器使用了一些技术手段可以在n个操作系统线程上多工调度m个Goroutine。Go调度器的工作和内核的调度是相似的但是这个调度器只关注单独的Go程序中的Goroutine。Goroutine采用的是半抢占式的协作调度只有但当前Goroutine发生阻塞时才会导致调度同时发生在用户态调度器会根据具体函数只保存必要的寄存器切换的代价要比系统线程低得多。运行时有一个`runtime.GOMAXPROCS`变量用于控制当前运行正常非阻塞Goroutine的系统线程数目。
在Go语言中启动一个Goroutine不仅和调用函数一样简单而且Goroutine之间调度代价也很低这些因素极大地促进并发编程的流行和发展。
在Go语言中启动一个Goroutine不仅和调用函数一样简单而且Goroutine之间调度代价也很低这些因素极大地促进并发编程的流行和发展。
## 原子操作
@ -51,9 +51,9 @@ func main() {
}
```
`worker`的循环中,为了保证`total.value += i`的原子性,我们通过`sync.Mutex`加锁和解锁来保证该语句在同一时刻只有一个线程可以访问。对于多线程模型的程序而言,进出临界区前后进行加锁和解锁都是必须的。如果没有锁的保护,`total`结果将由于多线程之间的竞争而导致错误结果
`worker`的循环中,为了保证`total.value += i`的原子性,我们通过`sync.Mutex`加锁和解锁来保证该语句在同一时刻只被一个线程访问。对于多线程模型的程序而言,进出临界区前后进行加锁和解锁都是必须的。如果没有锁的保护,`total`最终值将由于多线程之间的竞争而可能会不正确
用互斥锁来保护一个数值型的共享资源,不仅仅编写麻烦效率也很低下。其实标准库的`sync/atomic`已经对原子操作提供了丰富的支持。我们可以重新实现上面的例子:
用互斥锁来保护一个数值型的共享资源,麻烦效率低下。标准库的`sync/atomic`包对原子操作提供了丰富的支持。我们可以重新实现上面的例子:
```go
import (
@ -91,7 +91,7 @@ var (
mu sync.Mutex
)
func GetInstance() *singleton {
func Instance() *singleton {
if atomic.LoadUInt32(&initialized) == 1 {
return instance
}
@ -138,7 +138,7 @@ var (
once sync.Once
)
func GetInstance() *singleton {
func Instance() *singleton {
once.Do(func() {
instance = &singleton{}
})
@ -146,7 +146,7 @@ func GetInstance() *singleton {
}
```
`sync/atomic`不仅仅对基本的数值类型提供的原子操作的支持,而且对复杂对象的读写也提供了原子操作的支持。`atomic.Value`原子对象提供了`Load``Store`两个原子方法,分别用于加载和保存数据,返回值和参数都是`interface{}`类型,因此可以用于任意的自定义复杂类型。
`sync/atomic`对基本的数值类型及复杂对象的读写都提供了原子操作的支持。`atomic.Value`原子对象提供了`Load``Store`两个原子方法,分别用于加载和保存数据,返回值和参数都是`interface{}`类型,因此可以用于任意的自定义复杂类型。
```go
var config atomic.Value // 保存当前配置信息
@ -162,7 +162,7 @@ go func() {
}
}()
// 用于处理每个请求的工作者线程始终采用最新的配置信息
// 用于处理请求的工作者线程始终采用最新的配置信息
for i := 0; i < 10; i++ {
go func() {
for r := range requests() {
@ -177,7 +177,7 @@ for i := 0; i < 10; i++ {
## 顺序一致性内存模型
如果只是简单地在线程之间进行数据同步的话,原子操作已经为编程人员提供了一些同步保障。不过这种安全所基于的假设前提是:顺序一致性的内存模型。要了解顺序一致性,我们先看看一个简单的例子:
如果只是简单地在线程之间进行数据同步的话,原子操作已经为编程人员提供了一些同步保障。不过这种保障有一个前提:顺序一致性的内存模型。要了解顺序一致性,我们先看看一个简单的例子:
```go
var a string
@ -197,9 +197,9 @@ func main() {
我们创建了`setup`线程,用于对字符串`a`的初始化工作,初始化完成之后设置`done`标志为`true``main`函数所在的主线程中,通过`for !done {}`检测`done`变为`true`时,认为字符串初始化工作完成,然后进行字符串的打印工作。
但是Go语言并不保证在`main`函数中观测到的对`done`写入操作发生在对字符串`a`写入的操作之后,因此程序很可能打印一个空字符串。更糟糕的是,因为两个线程之间没有同步事件,`setup`线程对`done`的写入操作甚至无法被`main`线程看到,`main`函数有可能陷入死循环中。
但是Go语言并不保证在`main`函数中观测到的对`done`的写入操作发生在对字符串`a`写入的操作之后,因此程序很可能打印一个空字符串。更糟糕的是,因为两个线程之间没有同步事件,`setup`线程对`done`的写入操作甚至无法被`main`线程看到,`main`函数有可能陷入死循环中。
在Go语言中同一个Goroutine线程内部顺序一致性内存模型依然是得到保证的。但是不同的Goroutine之间并不满足顺序一致性内存模型需要通过明确定义的同步事件来作为同步的参考。如果两个事件不可排序那么就说这两个事件是并发的。为了最大化并行Go语言的编译器和处理器在不影响上述规定的前提下可能会对执行语句重新排序CPU也会对一些指令进行乱序执行
在Go语言中同一个Goroutine线程内部顺序一致性内存模型是得到保证的。但是不同的Goroutine之间并不满足顺序一致性内存模型需要通过明确定义的同步事件来作为同步的参考。如果两个事件不可排序那么就说这两个事件是并发的。为了最大化并行Go语言的编译器和处理器在不影响上述规定的前提下可能会对执行语句重新排序CPU也会对一些指令进行乱序执行
因此如果在一个Goroutine中顺序执行`a = 1; b = 2;`两个语句虽然在当前的Goroutine中可以认为`a = 1;`语句先于`b = 2;`语句执行但是在另一个Goroutine中`b = 2;`语句可能会先于`a = 1;`语句执行甚至在另一个Goroutine中无法看到它们的变化可能始终在寄存器中。也就是说在另一个Goroutine看来, `a = 1; b = 2;`两个语句的执行顺序是不确定的。如果一个并发程序无法确定事件的偏序关系,那么程序的运行结果往往会有不确定的结果。比如下面这个程序:
@ -302,7 +302,7 @@ func main() {
可保证打印出“hello, world”。该程序首先对`msg`进行写入,然后在`done`管道上发送同步信号,随后从`done`接收对应的同步信号,最后执行`println`函数。
若在关闭关闭后从中接收数据,接收者就会收到该信道返回的零值。因此在这个例子中,用`close(c)`关闭管道代替`done <- false`依然能保证该程序产生相同的行为。
若在关闭信道后继续从中接收数据,接收者就会收到该信道返回的零值。因此在这个例子中,用`close(c)`关闭管道代替`done <- false`依然能保证该程序产生相同的行为。
```go
var done = make(chan bool)