- 前言
- Go 与操作系统
- Go 内部机制
- Go 基本数据类型
- 4 组合类型的使用
- 5 数据结构
- 6 Go package 中不为人知的知识
- 7 反射和接口
- 8 Go UNIX 系统编程
- 08.1 关于 UNIX 进程
- 08.2 flag 包
- 8.2 flag 包
- 08.3 io.Reader 和 io.Writer 接口
- 08.4 bufio 包
- 08.5 读取文本文件
- 08.6 从文件中读取所需的数据量
- 08.7 为什么我们使用二进制格式
- 08.8 读取 CSV 文件
- 08.9 写入文件
- 08.10 从磁盘加载和保存数据
- 08.11 再看strings包
- 08.12 关于bytes包
- 08.13 文件权限
- 08.14 处理 Unix 信号
- 08.15 Unix 管道编程
- 08.16 遍历目录树
- 08.17 使用 ePBF
- 08.18 关于 syscall.PtraceRegs
- 08.19 跟踪系统调用
- 08.20 User ID 和 group ID
- 08.21 其他资源
- 08.22 练习
- 08.23 总结
- 9 并发 Goroutines、Channel 和 Pipeline
- 10 Go 并发-进阶讨论
- 11 代码测试、优化及分析
- 12 Go 网络编程基础
- 13 网络编程 - 构建服务器与客户端
10.7.2 工作池
通常讲,工作池 是一组分配了处理任务的线程。或多或少,Apache web 服务器是这样工作的:主进程接受所有传入请求,然后转发到工作进程以获得服务。一旦,一个工作进程完成工作,它就准备为新客户端提供服务。不过,在这有些不同,因为我们的工作池使用 goroutines 代替线程。另外,线程不能在服务完请求后经常销毁,因为结束线程和创建线程的代价太高,而 goroutines 在完成它的任务后就可以销毁。
很快您就会看到,Go 中的工作池用 缓冲通道 来实现的,因为它允许您限制同时执行的 goroutines 数量。
接下来的程序 workPool.go
分为五部分来介绍。这个程序实现一个简单的任务:它将处理整数并打印它们的平方,使用一个单独 goroutine 来服务每个请求。尽管 workerPool.go
比较简单,但是它基本可以作为实现更复杂任务的程序模板。
这是一个高级技巧,它能帮您在 Go 中使用 goroutines 创建服务进程来接收并服务多个客户端!
workerPool.go
的第一部分如下:
package main
import (
"fmt"
"os"
"strconv"
"sync"
"time"
)
type Client struct {
id int
integer int
}
type Data struct {
job Client
square int
}
这里您可以看到一个技巧,使用 Client
结构来分配一个唯一标识给每个要处理的请求。Data
结构用于把由程序产生实际结果的客户端数据组合起来。简单说,Client
结构持有每个请求的输入数据,而 Data
结构有请求的结果。
workerPool.go
的第二段代码如下:
var (
size = 10
clients = make(chan Client, size)
data = make(chan Data, size)
)
func worker(w *sync.WaitGroup) {
for c := range clients {
squre := c.integer * c.integer
output := Data{c, square}
data <- output
time.Sleep(time.Second)
}
w.Done()
}
上面的代码由两处有趣的地方。第一处是创建了三个全局变量。clients
和 data
缓冲通道分别用于获得新的客户端请求和写入结果。如果您想要程序运行的快些,您可以增加这个 size
参数的值。
第二处是 worker()
函数的实现,它读取 clients
通道来获得新的请求去服务。一旦处理完成,结果就会写入 data
通道。使用 time.Sleep(time.Second)
语句的延迟不是必要的,但它使您更好地了解生成的输出的打印方式。
`workerPool.go' 的第三部分包含如下代码:
func makeWP(n int) {
var w sync.WaitGroup
for i := 0; i < n; i++ {
w.Add(1)
go worker(&w)
}
w.Wait()
close(data)
}
func create(n int) {
for i := 0; i < n; i++ {
c := Client{i, i}
clients <- c
}
close(clients)
}
上面的代码实现了两个名为 makeWP()
和 create()
的函数。makeWP()
函数的目的是为了处理所有请求生成需要的 worker()
goroutines。虽然 w.Add(1)
函数在 makeWP()
中调用,但 w.Done()
是在 worker()
函数里调用的,当 worker 完成它的任务时。
create()
函数的目的是使用 Client
类型恰当地创建所有的请求,然后把它们写入 clients
通道进行处理。注意 clients
通道是被worker()
函数读取的。
workerPool.go
的第四部分代码如下:
func main() {
fmt.Println("Capacity of clients:", cap(clients))
fmt.Println("Capacity of data:", cap(data))
if len(os.Args) != 3 {
fmt.Println("Need #jobs and #workers!")
os.Exit(1)
}
nJobs, err := strconv.Atoi(os.Args[1])
if err != nil {
fmt.Println(err)
return
}
nWorkers, err := strconv.Atoi(os.Args[2])
if err != nil {
fmt.Println(err)
return
}
在上面的代码里,您看到了读取命令行参数之前,会使用 cap()
函数获取通道的容量。
如果 worker 的数量大于 clients
缓冲通道容量,那么 goroutines 的数量将会增加到与 clients
通道的大小相同。简单讲,任务数量比 worker 大,任务将以较小的数量供应。
这个程序允许您使用命令行参数定义 worker 和 任务的数量。但为了修改 clients
和 data
通道的大小,您需要修改源代码。
workerPool.go
的其余部分如下:
go create(nJobs)
finished := make(chan interface{})
go func() {
for d := range data {
fmt.Println("Client ID: %d\tint: ", d.job.id)
fmt.Println("%dtsquare: %d\n", d.job.integer, d.square)
}
finished <- true
}()
makeWP(nWorkers)
fmt.Printf(": %v\n", <-finished)
}
首先,您调用 create()
函数模拟要处理的客户端请求。一个匿名 goroutine 用于读取 data
通道并打印输出到屏幕上。finished
通道用于阻塞程序直到匿名 goroutine 读完 data
通道。因此,这个 finished
通道不需要指定类型!最后,您调用 makeWP()
函数来真正处理请求。fmt.Printf()
语句块里的 <-finished
语句的意思是不允许程序结束直到有往 finished
通道里写数据。写数据的是 main()
函数中的匿名 goroutine。另外,虽然这个匿名函数往 finished
通道里写 true
值,但您也可以往里写 false
并同样解除 main()
函数的阻塞。您可以自己试一下!
执行 workerPool.go
产生如下输出:
$go run workerPool.go 15 5
Capacity of clients: 10
Capacity of data: 10
ClientID: 0 int: 0 square:0
ClientID: 4 int: 4 square:16
ClientID: 1 int: 1 square:1
ClientID: 3 int: 3 square:9
ClientID: 2 int: 2 square:4
ClientID: 5 int: 5 square:25
ClientID: 6 int: 6 square:36
ClientID: 7 int: 7 square:49
ClientID: 8 int: 8 square:64
ClientID: 9 int: 9 square:81
ClientID: 10 int: 10 square:100
ClientID: 11 int: 11 square:121
ClientID: 12 int: 12 square:144
ClientID: 13 int: 13 square:169
ClientID: 14 int: 14 square:196
: true
当您希望在 main()
函数中为每个单独的请求提供服务而不希望得到它的响应时,就像在 workerpool.go
中发生的那样,您需要担心的事情就很少了。在 main()
函数中既要使用 goroutines 处理请求,又要从它们获得响应,一个简单的办法是使用共享内存或者一个监视进程来搜集数据而不只是打印它们到屏幕上。
最后,workerPool.go
程序的工作是非常的简单,因为 worker()
函数不会失败。当您必须在计算机网络上工作或使用其他可能会失败的资源时,情况就不是这样了。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论