第十四章下部分 (#835)

This commit is contained in:
Haigang Zhou
2022-05-17 16:26:06 +08:00
committed by GitHub
parent f08d76efa6
commit 60fe3dd076
7 changed files with 28 additions and 23 deletions

View File

@@ -1,6 +1,6 @@
# 14.11 限制同时处理的请求数
使用带缓冲区的通道很容易实现这一点(参见 [14.2.5](14.2.md#1425-%E5%90%8C%E6%AD%A5%E9%80%9A%E9%81%93-%E4%BD%BF%E7%94%A8%E5%B8%A6%E7%BC%93%E5%86%B2%E7%9A%84%E9%80%9A%E9%81%93)),其缓冲区容量就是同时处理请求的最大数量。程序 [max_tasks.go](examples/chapter_14/max_tasks.go) 虽然没有做什么有用的事但是却包含了这个技巧:超过 `MAXREQS` 的请求将不会被同时处理,因为当信号通道表示缓冲区已满时 `handle` 函数会阻塞且不再处理其他请求,直到某个请求从 `sem` 中被移除。`sem` 就像一个信号量,这一专业术语用于在程序中表示特定条件的标志变量。
使用带缓冲区的通道很容易实现这一点(参见 [14.2.5](14.2.md#1425-%E5%90%8C%E6%AD%A5%E9%80%9A%E9%81%93-%E4%BD%BF%E7%94%A8%E5%B8%A6%E7%BC%93%E5%86%B2%E7%9A%84%E9%80%9A%E9%81%93)),其缓冲区容量就是同时处理请求的最大数量。程序 [max_tasks.go](examples/chapter_14/max_tasks.go) 虽然没有做什么有用的事但是却包含了这个技巧:超过 `MAXREQS` 的请求将不会被同时处理,因为当信号通道表示缓冲区已满时 `handle()` 函数会阻塞且不再处理其他请求,直到某个请求从 `sem` 中被移除。`sem` 就像一个信号量,这一专业术语用于在程序中表示特定条件的标志变量。
示例14.16-[max_tasks.go](examples/chapter_14/max_tasks.go)
```go

View File

@@ -1,11 +1,10 @@
# 14.12 链式协程
下面的演示程序 [chaining.go](examples/chapter_14/chaining.go) 再次展示了启动巨量的 Go 协程是多么容易。这些协程已全部在 main 函数中的 for
循环里启动。当循环完成之后,一个 0 被写入到最右边的通道里,于是 100,000 个协程开始执行,接着 `1000000` 这个结果会在 1.5 秒之内被打印出来。
下面的演示程序 [chaining.go](examples/chapter_14/chaining.go) 再次展示了启动巨量的 Go 协程是多么容易。这些协程已全部在 `main()` 函数中的 `for` 循环里启动。当循环完成之后,一个 `0` 被写入到最右边的通道里,于是 100,000 个协程开始执行,接着 `1000000` 这个结果会在 1.5 秒之内被打印出来。
这个程序同时也展示了如何通过 `flag.Int` 来解析命令行中的参数以指定协程数量,例如:`chaining -n=7000` 会生成 7000 个协程。
示例 14.17-[chaining.go](examples/chapter_14/chaining.go)
```go
@@ -33,17 +32,21 @@ func main() {
fmt.Println(x) // 100000, about 1.5 s
}
```
*译者注:原本认为 leftmost 的结果为 1 ,认为只在最初做了一次赋值,实际结果为 100000无缓存信道具有同步阻塞的特性*
*1. 主线程的 right <- 0right 不是最初循环的那个 right而是最终循环的 right*
译者注:原本认为 `leftmost` 的结果为 `1` ,认为只在最初做了一次赋值,实际结果为 `100000`(无缓存信道具有同步阻塞的特性)
*2. for 循环中最初的 go f(left, right) 因为没有发送者一直处于等待状态*
1. 主线程的 `right <- 0`right 不是最初循环的那个 `right`,而是最终循环的 `right`
*3. 当主线程的 right <- 0 执行时,类似于递归函数在最内层产生返回值一般*
2. `for` 循环中最初的 `go f(left, right)` 因为没有发送者一直处于等待状态
3. 当主线程的 `right <- 0` 执行时,类似于递归函数在最内层产生返回值一般
## 链接
- [目录](directory.md)
- 上一节:[限制同时处理的请求数](14.11.md)
- 下一节:[在多核心上并行计算](14.13.md)

View File

@@ -1,5 +1,5 @@
# 14.13 在多核心上并行计算
假设我们有 `NCPU` 个 CPU 核心:`const NCPU = 4 //对应一个四核处理器` 然后我们想把计算量分成 `NCPU` 个部分,每一个部分都和其他部分并行运行。
这可以通过以下代码所示的方式完成(我们且省略具体参数)
@@ -28,9 +28,9 @@ func main() {
}
```
- `DoAll()` 函数创建了一个 `sem` 通道,每个并行计算都将在对其发送完成信号;在一个 for 循环中 `NCPU` 个协程被启动了,每个协程会承担 `1/NCPU` 的工作量。每一个 `DoPart()` 协程都会向 `sem` 通道发送完成信号。
- `DoAll()` 函数创建了一个 `sem` 通道,每个并行计算都将在对其发送完成信号;在一个 `for` 循环中 `NCPU` 个协程被启动了,每个协程会承担 `1/NCPU` 的工作量。每一个 `DoPart()` 协程都会向 `sem` 通道发送完成信号。
- `DoAll()` 会在 for 循环中等待 `NCPU` 个协程完成:`sem` 通道就像一个信号量,这份代码展示了一个经典的信号量模式。(参见 [14.2.7](14.2.md#1427-%E4%BF%A1%E5%8F%B7%E9%87%8F%E6%A8%A1%E5%BC%8F)
- `DoAll()` 会在 `for` 循环中等待 `NCPU` 个协程完成:`sem` 通道就像一个信号量,这份代码展示了一个经典的信号量模式。(参见 [14.2.7](14.2.md#1427-%E4%BF%A1%E5%8F%B7%E9%87%8F%E6%A8%A1%E5%BC%8F)
在以上运行模型中,您还需将 `GOMAXPROCS` 设置为 `NCPU`(参见 [14.1.3](14.1.md#1413-%E4%BD%BF%E7%94%A8-gomaxprocs))。

View File

@@ -1,6 +1,8 @@
# 14.14 并行化大量数据的计算
假设我们需要处理一些数量巨大且互不相关的数据项,它们从一个 `in` 通道被传递进来,当我们处理完以后又要将它们放入另一个 `out` 通道,就像一个工厂流水线一样。处理每个数据项也可能包含许多步骤:Preprocess预处理 / StepA步骤A / StepB步骤B / ... / PostProcess后处理
假设我们需要处理一些数量巨大且互不相关的数据项,它们从一个 `in` 通道被传递进来,当我们处理完以后又要将它们放入另一个 `out` 通道,就像一个工厂流水线一样。处理每个数据项也可能包含许多步骤:
Preprocess预处理 / StepA步骤A / StepB步骤B / ... / PostProcess后处理
一个典型的用于解决按顺序执行每个步骤的顺序流水线算法可以写成下面这样:
@@ -15,7 +17,7 @@ func SerialProcessData(in <-chan *Data, out chan<- *Data) {
}
```
一次只执行一个步骤,并且按顺序处理每个项目:在第 1 个项目没有被 `PostProcess` 并放入 `out` 通道之前绝不会处理第 2 个项目。
一次只执行一个步骤,并且按顺序处理每个项目:在第 1 个项目没有被 `PostProcessData()` 并放入 `out` 通道之前绝不会处理第 2 个项目。
如果你仔细想想,你很快就会发现这将会造成巨大的时间浪费。

View File

@@ -4,10 +4,10 @@
考虑以下的客户端-服务器结构:客户端协程执行一个无限循环从某个源头(也许是网络)接收数据;数据读取到 `Buffer` 类型的缓冲区。为了避免分配过多的缓冲区以及释放缓冲区,它保留了一份空闲缓冲区列表,并且使用一个缓冲通道来表示这个列表:`var freeList = make(chan *Buffer,100)`
这个可重用的缓冲区队列freeList与服务器是共享的。 当接收数据时,客户端尝试从 `freeList` 获取缓冲区;但如果此时通道为空,则会分配新的缓冲区。一旦消息被加载后,它将被发送到服务器上的 `serverChan` 通道:
这个可重用的缓冲区队列 (`freeList`) 与服务器是共享的。 当接收数据时,客户端尝试从 `freeList` 获取缓冲区;但如果此时通道为空,则会分配新的缓冲区。一旦消息被加载后,它将被发送到服务器上的 `serverChan` 通道:
```go
var serverChan = make(chan *Buffer)
var serverChan = make(chan *Buffer)
```
以下是客户端的算法代码:
@@ -49,7 +49,7 @@ func server() {
}
```
但是这种方法在 `freeList` 通道已满的时候是行不通的,因为无法放入空闲 `freeList` 通道的缓冲区会被“丢到地上”由垃圾收集器回收(故名:漏桶算法)
但是这种方法在 `freeList` 通道已满的时候是行不通的,因为无法放入空闲 `freeList` 通道的缓冲区会被“丢到地上”由垃圾收集器回收(故名:漏桶算法)
## 链接

View File

@@ -1,6 +1,6 @@
# 14.16 对 Go 协程进行基准测试
在 [13.7 节](13.7.md) 我们提到了在 Go 语言中对你的函数进行基准测试。在此我们将其应用到一个用协程向通道写入整数再读出的实例中。这个函数将通过 `testing.Benchmark` 调用 `N` 次(例如:`N = 1,000,000``BenchMarkResult` 有一个 `String()` 方法来输出其结果。`N` 的值将由 `gotest` 来判断并取得一个足够大的数字,以获得合理的基准测试结果。当然同样的基准测试方法也适用于普通函数。
在 [13.7 节](13.7.md) 我们提到了在 Go 语言中对你的函数进行基准测试。在此我们将其应用到一个用协程向通道写入整数再读出的实例中。这个函数将通过 `testing.Benchmark()` 调用 `N` 次(例如:`N = 1,000,000``BenchMarkResult` 有一个 `String()` 方法来输出其结果。`N` 的值将由 `gotest` 来判断并取得一个足够大的数字,以获得合理的基准测试结果。当然同样的基准测试方法也适用于普通函数。
如果你想排除指定部分的代码或者更具体的指定要测试的部分,可以使用 `testing.B.startTimer()``testing.B.stopTimer()` 来开始或结束计时器。基准测试只有在所有的测试通过后才能运行!
@@ -52,10 +52,10 @@ func BenchmarkChannelBuffered(b *testing.B) {
buffered 1000000 4850 ns/op --> 810 477 / s
Linux:
```
## 链接
- [目录](directory.md)
- 上一节:[漏桶算法](14.15.md)
- 下一节:[使用通道并发访问对象](14.17.md)

View File

@@ -2,7 +2,7 @@
为了保护对象被并发访问修改,我们可以使用协程在后台顺序执行匿名函数来替代使用同步互斥锁。在下面的程序中我们有一个类型 `Person` 中包含一个字段 `chF` ,这是一个用于存放匿名函数的通道。
这个结构在构造函数 `NewPerson()` 中初始化的同时会启动一个后台协程 `backend()``backend()` 方法会在一个无限循环中执行 `chF` 中放置的所有函数,有效地将它们序列化从而提供了安全的并发访问。更改和读取 `salary` 的方法会通过将一个匿名函数写入 `chF` 通道中,然后让`backend()`按顺序执行以达到其目的。需注意的是 `Salary` 方法创建的闭包函数是如何将 `fChan` 通道包含在其中的。
这个结构在构造函数 `NewPerson()` 中初始化的同时会启动一个后台协程 `backend()``backend()` 方法会在一个无限循环中执行 `chF` 中放置的所有函数,有效地将它们序列化从而提供了安全的并发访问。更改和读取 `salary` 的方法会通过将一个匿名函数写入 `chF` 通道中,然后让 `backend()` 按顺序执行以达到其目的。需注意的是 `Salary()` 方法创建的闭包函数是如何将 `fChan` 通道包含在其中的。
当然,这是一个简化的例子,它不应该被用在这种案例下。但是它却向我们展示了在更复杂的场景中该如何解决这种问题。
@@ -72,4 +72,4 @@ Person - name is: Smith Bill - salary is: 4000.25
- [目录](directory.md)
- 上一节:[对Go协程进行基准测试](14.16.md)
- 下一章:[网络,模板和网页应用](15.0.md)