1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-24 12:32:21 +00:00

Merge pull request #270 from fuwensun/pr1-5-1f

ch1-5-fix typos
This commit is contained in:
chai2010 2018-08-08 20:29:44 +08:00 committed by GitHub
commit 09c9fabcae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,6 +1,6 @@
# 1.5 面向并发的内存模型
在早期CPU都是以单核的形式顺序执行机器指令。Go语言的祖先C语言正是这种顺序编程语言的代表。顺序编程语言中的顺序是指:所有的指令都是以串行的方式执行在相同的时刻有且仅有一个CPU在顺序执行程序的指令。
在早期CPU都是以单核的形式顺序执行机器指令。Go语言的祖先C语言正是这种顺序编程语言的代表。顺序编程语言中的顺序是指所有的指令都是以串行的方式执行在相同的时刻有且仅有一个CPU在顺序执行程序的指令。
随着处理器技术的发展单核时代以提升处理器频率来提高运行效率的方式遇到了瓶颈目前各种主流的CPU频率基本被锁定在了3GHZ附近。单核CPU的发展的停滞给多核CPU的发展带来了机遇。相应地编程语言也开始逐步向并行化的方向发展。Go语言正是在多核和网络化的时代背景下诞生的原生支持并发的编程语言。
@ -10,7 +10,7 @@
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的系统线程数目。
@ -18,9 +18,9 @@ Go的运行时还包含了其自己的调度器这个调度器使用了一些
## 1.5.2 原子操作
所谓的原子操作就是并发编程中“最小的且不可并行化”的操作。通常,有多个并发体对一个共享资源的操作是原子操作的话,同一时刻最多只能有一个并发体对该资源进行操作。从线程角度看,在当前线程修改共享资源期间,其它的线程是不能访问该资源的。原子操作对于多线程并发编程模型来说,不会发生有别于单线程的意外情况,共享资源的完整性可以得到保证。
所谓的原子操作就是并发编程中“最小的且不可并行化”的操作。通常,如果多个并发体对同一个共享资源进行的操作是原子的话,那么同一时刻最多只能有一个并发体对该资源进行操作。从线程角度看,在当前线程修改共享资源期间,其它的线程是不能访问该资源的。原子操作对于多线程并发编程模型来说,不会发生有别于单线程的意外情况,共享资源的完整性可以得到保证。
一般情况下,原子操作都是通过“互斥”访问来保证访问通常由特殊的CPU指令提供保护。当然如果仅仅是想模拟下粗粒度的原子操作我们可以借助于`sync.Mutex`来实现:
一般情况下原子操作都是通过“互斥”访问来保证的通常由特殊的CPU指令提供保护。当然如果仅仅是想模拟下粗粒度的原子操作我们可以借助于`sync.Mutex`来实现:
```go
import (
@ -179,7 +179,7 @@ for i := 0; i < 10; i++ {
}
```
这是一个简化的生产者消费者模型:后台线程生成最新的配置信息;前台多个工作者线程获取最新的配置信息。所有线程共享配置信息资源。
这是一个简化的生产者消费者模型:后台线程生成最新的配置信息;前台多个工作者线程获取最新的配置信息。所有线程共享配置信息资源。
## 1.5.3 顺序一致性内存模型
@ -207,7 +207,7 @@ func main() {
在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;`两个语句的执行顺序是不确定的。如果一个并发程序无法确定事件的序关系,那么程序的运行结果往往会有不确定的结果。比如下面这个程序:
因此如果在一个Goroutine中顺序执行`a = 1; b = 2;`两个语句虽然在当前的Goroutine中可以认为`a = 1;`语句先于`b = 2;`语句执行但是在另一个Goroutine中`b = 2;`语句可能会先于`a = 1;`语句执行甚至在另一个Goroutine中无法看到它们的变化可能始终在寄存器中。也就是说在另一个Goroutine看来, `a = 1; b = 2;`两个语句的执行顺序是不确定的。如果一个并发程序无法确定事件的序关系,那么程序的运行结果往往会有不确定的结果。比如下面这个程序:
```go