mirror of
https://github.com/unknwon/the-way-to-go_ZH_CN.git
synced 2025-08-12 03:06:41 +08:00
create 14.8.md
This commit is contained in:
@@ -18,7 +18,7 @@ type Task struct {
|
|||||||
Tasks []Task
|
Tasks []Task
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
sync.Mutex(参见9.3)是互斥锁:它用来在代码中保护临界区资源:同一时间只有一个协程(goroutine)可以进入该临界区。如果出现了同一时间多个协程都进入了该临界区,则会产生竞争:Pool结构就不能保证被正确更新。在传统的模式(在经典面向对象的语言中应用得比较多,比如C++,JAVA,C#)中,工作线程代码可能这样写:
|
sync.Mutex([参见9.3](09.3.md)是互斥锁:它用来在代码中保护临界区资源:同一时间只有一个go协程(goroutine)可以进入该临界区。如果出现了同一时间多个go协程都进入了该临界区,则会产生竞争:Pool结构就不能保证被正确更新。在传统的模式中(经典的面向对象的语言中应用得比较多,比如C++,JAVA,C#),worker代码可能这样写:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func Worker(pool *Pool) {
|
func Worker(pool *Pool) {
|
||||||
@@ -34,11 +34,11 @@ func Worker(pool *Pool) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
这些worker有许多都可以并发执行;他们当然可以在协程中启动。一个worker先将pool锁定,从pool获取第一项任务,再解锁和处理任务。加锁保证了同一时间只有个协程可以进入到pool中:一项任务有且只能被赋予一个work。如果不加锁,则工作协程可能会在`task:=pool.Task[0]`发生中断,导致`pool.Tasks=pool.Task[1:]`产出异常:一些工作协程获取不到任务,而一些任务可能被多个工作协程得到。加锁实现同步的方式在工作协程比较少时可以工作的很好,但是当工作协程池数量很大,任务量也很多时,处理效率将会因为频繁的加锁/解锁开销而降低。当工作协程数增加到一个阈值时,程序效率会急剧下降,这就成为了瓶颈。
|
这些worker有许多都可以并发执行;他们可以在go协程中启动。一个worker先将pool锁定,从pool获取第一项任务,再解锁和处理任务。加锁保证了同一时间只有一个go协程可以进入到pool中:一项任务有且只能被赋予一个worer。如果不加锁,则工作协程可能会在`task:=pool.Task[0]`发生切换,导致`pool.Tasks=pool.Task[1:]`结果异常:一些worker获取不到任务,而一些任务可能被多个worker得到。加锁实现同步的方式在工作协程比较少时可以工作的很好,但是当工作协程数量很大,任务量也很多时,处理效率将会因为频繁的加锁/解锁开销而降低。当工作协程数增加到一个阈值时,程序效率会急剧下降,这就成为了瓶颈。
|
||||||
|
|
||||||
新模式:使用通道
|
新模式:使用通道
|
||||||
|
|
||||||
使用通道进行同步:使用一个通道接受需要处理的任务,一个通道接受处理完成的任务(及其结果)。工作处理者在协程中启动,其数量N应该根据任务数量进行调整。
|
使用通道进行同步:使用一个通道接受需要处理的任务,一个通道接受处理完成的任务(及其结果)。worker在协程中启动,其数量N应该根据任务数量进行调整。
|
||||||
|
|
||||||
主线程扮演着Master节点角色,可能写成如下形式:
|
主线程扮演着Master节点角色,可能写成如下形式:
|
||||||
|
|
||||||
@@ -65,9 +65,9 @@ worker的逻辑比较简单:从pending通道拿任务,处理后将其放到d
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
这里并不使用锁:从通道得到新任务的过程没有任何竞争。随着任务数量增加,worker数量也应该相应增加,同时性能并不会像第一种方式那样下降明显。在pending通道中存在一份任务的拷贝,第一个worker从pending通道中获得第一个任务并进行处理,这里并不存在竞争(对一个通道读数据和写数据的整个过程是原子的:参见[14.2.2](14.2.md))。某一个任务会在哪一个worker中被执行是不可知的,反过来也是。worker数量的增多也会增加通信的开销,这会对性能有轻微的影响。
|
这里并不使用锁:从通道得到新任务的过程没有任何竞争。随着任务数量增加,worker数量也应该相应增加,同时性能并不会像第一种方式那样下降明显。在pending通道中存在一份任务的拷贝,第一个worker从pending通道中获得第一个任务并进行处理,这里并不存在竞争(对一个通道读数据和写数据的整个过程是原子性的:参见[14.2.2](14.2.md))。某一个任务会在哪一个worker中被执行是不可知的,反过来也是。worker数量的增多也会增加通信的开销,这会对性能有轻微的影响。
|
||||||
|
|
||||||
从这个简单的例子中可能很难看出第二种模式的优势,但含有复杂锁应用的程序不仅在编写上显得困难,也不容易编写正确,使用第二种模式的话,就无需考虑这么复杂的东西了。
|
从这个简单的例子中可能很难看出第二种模式的优势,但含有复杂锁运用的程序不仅在编写上显得困难,也不容易编写正确,使用第二种模式的话,就无需考虑这么复杂的东西了。
|
||||||
|
|
||||||
因此,第二种模式对比第一种模式而言,不仅性能是一个主要优势,而且还有个更大的优势:代码显得更清晰、更优雅。一个更符合go语言习惯的worker写法:
|
因此,第二种模式对比第一种模式而言,不仅性能是一个主要优势,而且还有个更大的优势:代码显得更清晰、更优雅。一个更符合go语言习惯的worker写法:
|
||||||
|
|
||||||
@@ -102,6 +102,6 @@ worker的逻辑比较简单:从pending通道拿任务,处理后将其放到d
|
|||||||
|
|
||||||
## 链接
|
## 链接
|
||||||
|
|
||||||
[目录](directory.md)
|
- [目录](directory.md)
|
||||||
上一节:[协程和恢复(recover)](14.6.md)
|
- 上一节:[协程和恢复(recover)](14.6.md)
|
||||||
下一节:[惰性生成器实现](14.8.md)
|
- 下一节:[惰性生成器实现](14.8.md)
|
59
eBook/14.8.md
Normal file
59
eBook/14.8.md
Normal 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)
|
Reference in New Issue
Block a user