This commit is contained in:
Xargin 2016-10-05 13:56:07 +08:00
parent 4991d9c975
commit 28fe19ecc7
6 changed files with 10 additions and 11 deletions

View File

@ -73,7 +73,7 @@ go func() { x = make([]int, 1000000) }()
x[999999] = 1 // NOTE: undefined behavior; memory corruption possible! x[999999] = 1 // NOTE: undefined behavior; memory corruption possible!
``` ```
最后一个语句中的x的值是未定义的其可能是nil或者也可能是一个长度为10的slice也可能是一个度为1,000,000的slice。但是回忆一下slice的三个组成部分指针(pointer)、长度(length)和容量(capacity)。如果指针是从第一个make调用来而长度从第二个make来x就变成了一个混合体一个自称长度为1,000,000但实际上内部只有10个元素的slice。这样导致的结果是存储999,999元素的位置会碰撞一个遥远的内存位置这种情况下难以对值进行预测而且定位和debug也会变成噩梦。这种语义雷区被称为未定义行为对C程序员来说应该很熟悉幸运的是在Go语言里造成的麻烦要比C里小得多。 最后一个语句中的x的值是未定义的其可能是nil或者也可能是一个长度为10的slice也可能是一个度为1,000,000的slice。但是回忆一下slice的三个组成部分指针(pointer)、长度(length)和容量(capacity)。如果指针是从第一个make调用来而长度从第二个make来x就变成了一个混合体一个自称长度为1,000,000但实际上内部只有10个元素的slice。这样导致的结果是存储999,999元素的位置会碰撞一个遥远的内存位置这种情况下难以对值进行预测而且debug也会变成噩梦。这种语义雷区被称为未定义行为对C程序员来说应该很熟悉幸运的是在Go语言里造成的麻烦要比C里小得多。
尽管并发程序的概念让我们知道并发并不是简单的语句交叉执行。我们将会在9.4节中看到数据竞争可能会有奇怪的结果。许多程序员甚至一些非常聪明的人也还是会偶尔提出一些理由来允许数据竞争比如“互斥条件代价太高”“这个逻辑只是用来做logging”“我不介意丢失一些消息”等等。因为在他们的编译器或者平台上很少遇到问题可能给了他们错误的信心。一个好的经验法则是根本就没有什么所谓的良性数据竞争。所以我们一定要避免数据竞争那么在我们的程序中要如何做到呢 尽管并发程序的概念让我们知道并发并不是简单的语句交叉执行。我们将会在9.4节中看到数据竞争可能会有奇怪的结果。许多程序员甚至一些非常聪明的人也还是会偶尔提出一些理由来允许数据竞争比如“互斥条件代价太高”“这个逻辑只是用来做logging”“我不介意丢失一些消息”等等。因为在他们的编译器或者平台上很少遇到问题可能给了他们错误的信心。一个好的经验法则是根本就没有什么所谓的良性数据竞争。所以我们一定要避免数据竞争那么在我们的程序中要如何做到呢

View File

@ -1,6 +1,6 @@
## 9.4. 内存同步 ## 9.4. 内存同步
你可能比较纠结为什么Balance方法需要用到互斥条件无论是基于channel还是基于互斥量。毕竟和存款不一样它只由一个简单的操作组成所以不会碰到其它goroutine在其执行""执行其它的逻辑的风险。这里使用mutex有两方面考虑。第一Balance不会在其它操作比如Withdraw“中间”执行。第二(更重要)的是"同步"不仅仅是一堆goroutine执行顺序的问题同样也会涉及到内存的问题。 你可能比较纠结为什么Balance方法需要用到互斥条件无论是基于channel还是基于互斥量。毕竟和存款不一样它只由一个简单的操作组成所以不会碰到其它goroutine在其执行"期间"执行其它的逻辑的风险。这里使用mutex有两方面考虑。第一Balance不会在其它操作比如Withdraw“中间”执行。第二(更重要)的是"同步"不仅仅是一堆goroutine执行顺序的问题同样也会涉及到内存的问题。
在现代计算机中可能会有一堆处理器,每一个都会有其本地缓存(local cache)。为了效率对内存的写入一般会在每一个处理器中缓冲并在必要时一起flush到主存。这种情况下这些数据可能会以与当初goroutine写入顺序不同的顺序被提交到主存。像channel通信或者互斥量操作这样的原语会使处理器将其聚集的写入flush并commit这样goroutine在某个时间点上的执行结果才能被其它处理器上运行的goroutine得到。 在现代计算机中可能会有一堆处理器,每一个都会有其本地缓存(local cache)。为了效率对内存的写入一般会在每一个处理器中缓冲并在必要时一起flush到主存。这种情况下这些数据可能会以与当初goroutine写入顺序不同的顺序被提交到主存。像channel通信或者互斥量操作这样的原语会使处理器将其聚集的写入flush并commit这样goroutine在某个时间点上的执行结果才能被其它处理器上运行的goroutine得到。

View File

@ -54,7 +54,7 @@ func (memo *Memo) Get(key string) (interface{}, error) {
} }
``` ```
Memo实例会记录需要缓存的函数f(类型为Func),以及缓存内容(里面是一个string到result映射的map)。每一个result都是都是简单的函数返回的值对儿--一个值和一个错误值。继续下去我们会展示一些Memo的变种不过所有的例子都会遵循这些上面的这些方面。 Memo实例会记录需要缓存的函数f(类型为Func),以及缓存内容(里面是一个string到result映射的map)。每一个result都是简单的函数返回的值对儿--一个值和一个错误值。继续下去我们会展示一些Memo的变种不过所有的例子都会遵循这些上面的这些方面。
下面是一个使用Memo的例子。对于流入的URL的每一个元素我们都会调用Get并打印调用延时以及其返回的数据大小的log 下面是一个使用Memo的例子。对于流入的URL的每一个元素我们都会调用Get并打印调用延时以及其返回的数据大小的log
@ -163,7 +163,6 @@ type Memo struct {
// Get is concurrency-safe. // Get is concurrency-safe.
func (memo *Memo) Get(key string) (value interface{}, err error) { func (memo *Memo) Get(key string) (value interface{}, err error) {
memo.cache[key] = res
memo.mu.Lock() memo.mu.Lock()
res, ok := memo.cache[key] res, ok := memo.cache[key]
if !ok { if !ok {
@ -245,7 +244,7 @@ func (memo *Memo) Get(key string) (value interface{}, err error) {
现在Get函数包括下面这些步骤了获取互斥锁来保护共享变量cache map查询map中是否存在指定条目如果没有找到那么分配空间插入一个新条目释放互斥锁。如果存在条目的话且其值没有写入完成(也就是有其它的goroutine在调用f这个慢函数)时goroutine必须等待值ready之后才能读到条目的结果。而想知道是否ready的话可以直接从ready channel中读取由于这个读取操作在channel关闭之前一直是阻塞。 现在Get函数包括下面这些步骤了获取互斥锁来保护共享变量cache map查询map中是否存在指定条目如果没有找到那么分配空间插入一个新条目释放互斥锁。如果存在条目的话且其值没有写入完成(也就是有其它的goroutine在调用f这个慢函数)时goroutine必须等待值ready之后才能读到条目的结果。而想知道是否ready的话可以直接从ready channel中读取由于这个读取操作在channel关闭之前一直是阻塞。
如果没有条目的话需要向map中插入一个没有ready的条目当前正在调用的goroutine就需要负责调用慢函数、更新条目以及向其它所有goroutine广播条目已经ready可读的消息了。 如果没有条目的话需要向map中插入一个没有准备好的条目当前正在调用的goroutine就需要负责调用慢函数、更新条目以及向其它所有goroutine广播条目已经ready可读的消息了。
条目中的e.res.value和e.res.err变量是在多个goroutine之间共享的。创建条目的goroutine同时也会设置条目的值其它goroutine在收到"ready"的广播消息之后立刻会去读取条目的值。尽管会被多个goroutine同时访问但却并不需要互斥锁。ready channel的关闭一定会发生在其它goroutine接收到广播事件之前因此第一个goroutine对这些变量的写操作是一定发生在这些读操作之前的。不会发生数据竞争。 条目中的e.res.value和e.res.err变量是在多个goroutine之间共享的。创建条目的goroutine同时也会设置条目的值其它goroutine在收到"ready"的广播消息之后立刻会去读取条目的值。尽管会被多个goroutine同时访问但却并不需要互斥锁。ready channel的关闭一定会发生在其它goroutine接收到广播事件之前因此第一个goroutine对这些变量的写操作是一定发生在这些读操作之前的。不会发生数据竞争。
@ -299,7 +298,7 @@ func (memo *Memo) Get(key string) (interface{}, error) {
func (memo *Memo) Close() { close(memo.requests) } func (memo *Memo) Close() { close(memo.requests) }
``` ```
上面的Get方法会创建一个response channel把它放进request结构中然后发送给monitor goroutine然后马上又会接收它。 上面的Get方法会创建一个response channel把它放进request结构中然后发送给monitor goroutine然后马上又会接收它。
cache变量被限制在了monitor goroutine (\*Memo).server中下面会看到。monitor会在循环中一直读取请求直到request channel被Close方法关闭。每一个请求都会去查询cache如果没有找到条目的话那么就会创建/插入一个新的条目。 cache变量被限制在了monitor goroutine (\*Memo).server中下面会看到。monitor会在循环中一直读取请求直到request channel被Close方法关闭。每一个请求都会去查询cache如果没有找到条目的话那么就会创建/插入一个新的条目。
@ -335,9 +334,9 @@ func (e *entry) deliver(response chan<- result) {
和基于互斥量的版本类似第一个对某个key的请求需要负责去调用函数f并传入这个key将结果存在条目里并关闭ready channel来广播条目的ready消息。使用(\*entry).call来完成上述工作。 和基于互斥量的版本类似第一个对某个key的请求需要负责去调用函数f并传入这个key将结果存在条目里并关闭ready channel来广播条目的ready消息。使用(\*entry).call来完成上述工作。
紧接着对同一个key的请求会发现map中已经有了存在的条目然后会等待结果变为ready并将结果从response发送给客户端的goroutien。上述工作是用(\*entry).deliver来完成的。对call和deliver方法的调用必须在自己的goroutine中进行以确保monitor goroutines不会因此而被阻塞住而没法处理新的请求。 紧接着对同一个key的请求会发现map中已经有了存在的条目然后会等待结果变为ready并将结果从response发送给客户端的goroutien。上述工作是用(\*entry).deliver来完成的。对call和deliver方法的调用必须让它们在自己的goroutine中进行以确保monitor goroutines不会因此而被阻塞住而没法处理新的请求。
这个例子说明我们无论可以用上锁,还是通信来建立并发程序都是可行的。 这个例子说明我们无论用上锁,还是通信来建立并发程序都是可行的。
上面的两种方案并不好说特定情境下哪种更好,不过了解他们还是有价值的。有时候从一种方式切换到另一种可以使你的代码更为简洁。(译注不是说好的golang推崇通信并发么) 上面的两种方案并不好说特定情境下哪种更好,不过了解他们还是有价值的。有时候从一种方式切换到另一种可以使你的代码更为简洁。(译注不是说好的golang推崇通信并发么)

View File

@ -1,6 +1,6 @@
### 9.8.3. GOMAXPROCS ### 9.8.3. GOMAXPROCS
Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数所以在一个有8个核心的机器上时调度器一次会在8个OS线程上去调度GO代码。(GOMAXPROCS是前面说的m:n调度中的n)。在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。在I/O中或系统调用中或调用非Go语言函数时是需要一个对应的操作系统线程的但是GOMAXPROCS并不需要将这几种情况计在内。 Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数所以在一个有8个核心的机器上时调度器一次会在8个OS线程上去调度GO代码。(GOMAXPROCS是前面说的m:n调度中的n)。在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。在I/O中或系统调用中或调用非Go语言函数时是需要一个对应的操作系统线程的但是GOMAXPROCS并不需要将这几种情况计在内。
你可以用GOMAXPROCS的环境变量来显式地控制这个参数或者也可以在运行时用runtime.GOMAXPROCS函数来修改它。我们在下面的小程序中会看到GOMAXPROCS的效果这个程序会无限打印0和1。 你可以用GOMAXPROCS的环境变量来显式地控制这个参数或者也可以在运行时用runtime.GOMAXPROCS函数来修改它。我们在下面的小程序中会看到GOMAXPROCS的效果这个程序会无限打印0和1。

View File

@ -6,5 +6,5 @@ goroutine没有可以被程序员获取到的身份(id)的概念。这一点是
Go鼓励更为简单的模式这种模式下参数对函数的影响都是显式的。这样不仅使程序变得更易读而且会让我们自由地向一些给定的函数分配子任务时不用担心其身份信息影响行为。 Go鼓励更为简单的模式这种模式下参数对函数的影响都是显式的。这样不仅使程序变得更易读而且会让我们自由地向一些给定的函数分配子任务时不用担心其身份信息影响行为。
你现在应该已经明白了写一个Go程序所需要的所有语言特性信息。在后面两章节中我们会回顾一些之前的实例和工具支持我们写出更大规模的程序如何将一个工程组织成一系列的包获取,构建,测试,性能测试,剖析,写文档,并且将这些包分享出去。 你现在应该已经明白了写一个Go程序所需要的所有语言特性信息。在后面两章节中我们会回顾一些之前的实例和工具支持我们写出更大规模的程序如何将一个工程组织成一系列的包获取,构建,测试,性能测试,剖析,写文档,并且将这些包分享出去。

View File

@ -1,5 +1,5 @@
# 第九章 基于共享变量的并发 # 第九章 基于共享变量的并发
前一章我们介绍了一些使用goroutine和channel这样直接而自然的方式来实现并发的方法。然而这样做我们实际上屏蔽掉了在写并发代码时必须处理的一些重要而且细微的问题。 前一章我们介绍了一些使用goroutine和channel这样直接而自然的方式来实现并发的方法。然而这样做我们实际上回避了在写并发代码时必须处理的一些重要而且细微的问题。
在本章中我们会细致地了解并发机制。尤其是在多goroutine之间的共享变量并发问题的分析手段以及解决这些问题的基本模式。最后我们会解释goroutine和操作系统线程之间的技术上的一些区别。 在本章中我们会细致地了解并发机制。尤其是在多goroutine之间的共享变量并发问题的分析手段以及解决这些问题的基本模式。最后我们会解释goroutine和操作系统线程之间的技术上的一些区别。