一般的计算机语言在处理并发任务时,经常会按需创建线程,例如在Web服务器应用中,接收到一个网络请求(network request)就会新建一个线程来处理。但系统资源有限,一般会控制所建线程个数的上限,而控制上限的一种方法就是使用“线程池”:例如我们把可用的n个线程抽象成一个线程池,该池中就有n个线程,需要处理请求时会从其中取出一个线程来使用,使用完毕后该线程将被放回池中,如果池中的线程都已经被取用,则新来的请求将被阻塞等待直至有可用的线程为止,另一种处理的方法是直接向该请求者报告异常,请求将被中止。在Go语言中线程对应的是goroutine,本节将演示如何用通道来实现goroutine池,由于没有必要直接将goroutine对应的函数直接放入通道中,因此也可用类似令牌的方式来控制goroutine的最大数量,形成虚拟的goroutine池,也可以看作将goroutine池转换成了令牌池。
package main
import (
"math/rand"
"time"
t "tools"
)
// Request1 是自定义的结构类型
// 用于发送请求时传递所需的所有数据
type Request1 struct {
ID int //本次请求的编码,是随机产生的整数
Count int //计算多少次
Response chan []string //用于存放请求结果的通道
}
// 令牌池
var poolG chan int
// 缓存请求的通道
var requestChannelG chan Request1
// 发送请求并等待请求执行结果的函数
func sendRequest(countA int) {
idT := rand.Intn(10000)
responseChanT := make(chan []string, 1)
defer close(responseChanT)
requestChannelG <- Request1{ID: idT, Count:countA, Response: responseChanT}
responseT := <-responseChanT
t.Printfln("goroutine(ID: %v, Count: %v)结果: %#v", idT, countA, responseT)
}
// 处理具体每个请求的函数
func doRequest(requestA Request1) {
resultT := 0
for i := 0; i < requestA.Count; i++ {
resultT = i + requestA.Count
}
requestA.Response <- []string{"请求成功", t.IntToString(resultT)}
poolG <- 0
}
// 处理请求队列(通道)的函数
// 根据令牌池是否有剩余令牌来决定是否启动处理具体请求的goroutine
func processRequests() {
for {
requestT := <-requestChannelG
if len(poolG) > 0 {
<-poolG
go doRequest(requestT)
} else {
requestT.Response <-[]string{"请求失败", "goroutine池已满"}
}
}
}
func main() {
//为令牌池分配容量,容量虽然为10
//但可用令牌是根据后面放入的令牌个数来确定的
poolG = make(chan int, 10)
//为请求缓冲通道分配100个的容量
//如果缓冲通道内超过100个请求,下一个请求将被阻塞
requestChannelG = make(chan Request1, 100)
//确保通道退出时会被关闭
defer close(requestChannelG)
defer close(poolG)
//启动请求处理主goroutine
go processRequests()
//在令牌池中放入5块令牌
for i := 0; i < 5; i++ {
poolG <- 0
}
//模拟发送20个请求
for i := 0; i < 20; i++ {
go sendRequest(1000)
}
//主线程死循环以保证处理请求的goroutine一直执行
//需要用Ctrl+C键来退出程序运行
for {
time.Sleep(100 * time.Millisecond)
}
}
代码 11‑9 goroutine2/goroutine2.go
代码11‑9中,使用通道poolG作为令牌池,来控制处理请求队列的主任务processRequests能够生成子任务(也就是由处理具体每个请求的doRequest函数生成的goroutine)的数量。代码中的要点说明如下:
-> poolG通道代表令牌池,里面有几块令牌就表明可以启动多少个处理请求的goroutine,也即是令牌池的可用令牌数限制了同时可以并发处理的请求数;注意poolG的容量并不代表可用令牌数,而是该通道中的数值项数表示令牌数;
-> requestChannelG通道用于缓存收到的所有请求,该通道的数值项类型是我们自定义的Request1,发送的请求必须符合该类型,Request1类型中的Response字段又是一个通道类型,用于接收该请求的响应信息;
-> sendRequest函数用于模拟创建单个的请求,它将产生一个随机的请求标识号填入Request1.ID字段中,然后指定要求计算的次数放入Request1.Count,还会创建用于接收响应信息的通道放入Request1.Response中,之后该函数就会通过该通道等待接收返回的响应信息后输出,最后退出该goroutine的运行;
-> processRequests函数用于处理请求队列和分派goroutine来处理,它会循环读取requestChannelG通道中的请求,一旦接收到一个请求时,它会首先判断是否还有可用的令牌,这是通过len(poolG)表达式来获取poolG通道的当前长度来判断的,如果不为0,则表明通道里面有数值项,也就代表还有可用令牌,此时通过<-poolG语句获取一块令牌,然后启动具体处理单个请求的doRequest函数的goroutine来进行处理;如果没有可用令牌,则直接向该请求的响应通道中写入相应信息表示请求失败;
-> doRequest函数用于处理单个请求,它在完成了指定的计算任务之后,会在该请求的响应通道中写入请求成功的信息和计算结果,然后在退出之前交还令牌(通过向poolG通道中写入一个数据来实现);
-> 主函数main中,首先定义了令牌池poolG通道和请求队列requestChannelG通道,并为它们分配了容量;然后启动processRequests函数的goroutine来循环处理请求;之后往令牌池poolG中放入了5块令牌,代表着本程序将能够同时并发处理5个请求;然后模拟发送了20个请求来测试其请求处理能力;最后用一个无限循环(也叫死循环)来保证程序一直运行,并通过time.Sleep函数来定时休眠一下,这样可以保证程序不占用太多的系统资源,并且在休眠时其他goroutine能得到更多的调度执行时间。注意,由于main函数最后的死循环,本程序必须用Ctrl-C快捷键才能终止运行。
该代码执行结果如下:
goroutine(ID: 3300, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 8081, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 1211, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 456, Count: 1000)结果: []string{"请求失败", "goroutine池已空"}
goroutine(ID: 2540, Count: 1000)结果: []string{"请求失败", "goroutine池已空"}
goroutine(ID: 7887, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 1847, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 2081, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 4425, Count: 1000)结果: []string{"请求失败", "goroutine池已空"}
goroutine(ID: 1318, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 694, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 8511, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 5089, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 8162, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 4728, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 3274, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 9106, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 1445, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 3237, Count: 1000)结果: []string{"请求成功", "1999"}
goroutine(ID: 4059, Count: 1000)结果: []string{"请求成功", "1999"}
可以看出,20个模拟发送的请求大部分被处理了,但有几个请求由于令牌池当时为空,所以没有被处理。
本例主要说明的要点包括:
-> 利用通道可以实现令牌池或虚拟的goroutine池等,用于控制goroutine等资源的数量上限,避免系统资源被无限制地使用;
-> 通道的数值项类型也可以是复合类型或者自定义的结构类型,这使得通道的可用性大大增强,例如,可以使用结构类型的通道在做goroutine归并时分别判断出不同类型的goroutine执行完毕的个数等。