Merge pull request #305 from liracle/master

翻译了14.7节
This commit is contained in:
无闻
2017-01-22 09:53:59 -05:00
committed by GitHub
2 changed files with 166 additions and 0 deletions

107
eBook/14.7.md Normal file
View File

@@ -0,0 +1,107 @@
# 14.7 新旧模型对比任务和worker
假设我们需要处理很多任务一个worker处理一项任务。任务可以被定义为一个结构体具体的细节在这里并不重要
```go
type Task struct {
// some state
}
```
旧模式:使用共享内存进行同步
由各个任务组成的任务池共享内存为了同步各个worker以及避免资源竞争我们需要对任务池进行加锁保护
```go
type Pool struct {
Mu sync.Mutex
Tasks []Task
}
```
sync.Mutex([参见9.3](09.3.md)是互斥锁它用来在代码中保护临界区资源同一时间只有一个go协程goroutine可以进入该临界区。如果出现了同一时间多个go协程都进入了该临界区则会产生竞争Pool结构就不能保证被正确更新。在传统的模式中经典的面向对象的语言中应用得比较多比如C++,JAVA,C#)worker代码可能这样写
```go
func Worker(pool *Pool) {
for {
pool.Mu.lock()
// begin critical section:
task := pool.Task[0] // take the first task
pool.Tasks = pool.Task[1:] // update the pool of tasks
// end critical section
pool.Mu.Unlock()
process(task)
}
}
```
这些worker有许多都可以并发执行他们可以在go协程中启动。一个worker先将pool锁定从pool获取第一项任务再解锁和处理任务。加锁保证了同一时间只有一个go协程可以进入到pool中一项任务有且只能被赋予一个worer。如果不加锁则工作协程可能会在`task:=pool.Task[0]`发生切换,导致`pool.Tasks=pool.Task[1:]`结果异常一些worker获取不到任务而一些任务可能被多个worker得到。加锁实现同步的方式在工作协程比较少时可以工作的很好但是当工作协程数量很大任务量也很多时处理效率将会因为频繁的加锁/解锁开销而降低。当工作协程数增加到一个阈值时,程序效率会急剧下降,这就成为了瓶颈。
新模式:使用通道
使用通道进行同步使用一个通道接受需要处理的任务一个通道接受处理完成的任务及其结果。worker在协程中启动其数量N应该根据任务数量进行调整。
主线程扮演着Master节点角色可能写成如下形式
```go
func main() {
pending, done := make(chan *Task), make(chan *Task)
go sendWork(pending) // put tasks with work on the channel
for i := 0; i < N; i++ { // start N goroutines to do work
go Worker(pending, done)
}
consumeWork(done) // continue with the processed tasks
}
```
worker的逻辑比较简单从pending通道拿任务处理后将其放到done通道中
```go
func Worker(in, out chan *Task) {
for {
t := <-in
process(t)
out <- t
}
}
```
这里并不使用锁从通道得到新任务的过程没有任何竞争。随着任务数量增加worker数量也应该相应增加同时性能并不会像第一种方式那样下降明显。在pending通道中存在一份任务的拷贝第一个worker从pending通道中获得第一个任务并进行处理这里并不存在竞争对一个通道读数据和写数据的整个过程是原子性的参见[14.2.2](14.2.md))。某一个任务会在哪一个worker中被执行是不可知的反过来也是。worker数量的增多也会增加通信的开销这会对性能有轻微的影响。
从这个简单的例子中可能很难看出第二种模式的优势,但含有复杂锁运用的程序不仅在编写上显得困难,也不容易编写正确,使用第二种模式的话,就无需考虑这么复杂的东西了。
因此第二种模式对比第一种模式而言不仅性能是一个主要优势而且还有个更大的优势代码显得更清晰、更优雅。一个更符合go语言习惯的worker写法
**IDIOM: Use an in- and out-channel instead of locking**
```go
func Worker(in, out chan *Task) {
for {
t := <-in
process(t)
out <- t
}
}
```
对于任何可以建模为Master-Worker范例的问题一个类似于worker使用通道进行通信和交互、Master进行整体协调的方案都能完美解决。如果系统部署在多台机器上各个机器上执行Worker协程Master和Worker之间使用netchan或者RPC进行通信参见15章
怎么选择是该使用锁还是通道?
通道是一个较新的概念本节我们着重强调了在go协程里通道的使用但这并不意味着经典的锁方法就不能使用。go语言让你可以根据实际问题进行选择创建一个优雅、简单、可读性强、在大多数场景性能表现都能很好的方案。如果你的问题适合使用锁也不要忌讳使用它。go语言注重实用什么方式最能解决你的问题就用什么方式而不是强迫你使用一种编码风格。下面列出一个普遍的经验法则
* 使用锁的情景:
- 访问共享数据结构中的缓存信息
- 保存应用程序上下文和状态信息数据
* 使用通道的情景:
- 与异步操作的结果进行交互
- 分发任务
- 传递数据所有权
当你发现你的锁使用规则变得很复杂时,可以反省使用通道会不会使问题变得简单些。
## 链接
- [目录](directory.md)
- 上一节:[协程和恢复recover](14.6.md)
- 下一节:[惰性生成器实现](14.8.md)

59
eBook/14.8.md Normal file
View File

@@ -0,0 +1,59 @@
# 14.8 惰性生成器的实现
生成器是指当被调用时返回一个序列中下一个值的函数,例如:
```go
generateInteger() => 0
generateInteger() => 1
generateInteger() => 2
....
```
生成器每次返回的是序列中下一个值而非整个序列这种特性也称之为惰性求值只在你需要时进行求值同时保留相关变量资源内存和cpu这是一项在需要时对表达式进行求值的技术。例如生成一个无限数量的偶数序列要产生这样一个序列并且在一个一个的使用可能会很困难而且内存会溢出但是一个含有通道和go协程的函数能轻易实现这个需求。
在14.12的例子中我们实现了一个使用int型通道来实现的生成器。通道被命名为`yield``resume`,这些此经常在协程代码中使用。
**Listing 14.12-lazy evaluation.go**
```go
package main
import (
"fmt"
)
var resume chan int
func integers() chan int {
yield := make(chan int)
count := 0
go func() {
for {
yield <- count
count++
}
}()
return yield
}
func generateInteger() int {
return <-resume
}
func main() {
resume = integers()
fmt.Println(generateInteger()) //=> 0
fmt.Println(generateInteger()) //=> 1
fmt.Println(generateInteger()) //=> 2
}
```
有一个细微的区别是从通道读取的值可能会是稍早前产生的并不是在程序被调用时生成的。如果确实需要这样的行为就得实现一个请求响应机制。当生成器生成数据的过程是计算密集型且各个结果的顺序并不重要时那么就可以将生成器放入到go协程实现并行化。但是得小心使用大量的go协程的开销可能会超过带来的性能增益。
这些原则可以概括为:通过巧妙地使用空接口、闭包和高阶函数,我们能实现一个通用的惰性生产器(这个应该放在一个工具包中实现)。
## 链接
- [目录](directory.md)
- 上一节:[新旧模型对比任务和worker](14.7.md)
- 下一节:[Implementing Futures](14.8.md)