Go并发编程实践
一、控制时间的高并发模型
可以测试接口在持续高并发情况下是否有句柄泄露
1.1、代码
package main
import (
"runtime"
"sync/atomic"
"time"
)
//5秒超时
var timeout2 = time.Now().Add(time.Second * 5)
//cpu数量
var cpuNum = runtime.NumCPU()
//无缓存
var waitForAll = make(chan struct{})
//无缓存
var done = make(chan struct{})
// 有缓存的管道,空闲的资源
var freeResource = make(chan struct{}, cpuNum)
var doneNum int64
func RunScenario2() {
// init free resource
initFreeResource()
go releaseResource()
go useResource()
//waitForAll 沒有数据会阻塞
// 等待所有的事情完成
<-waitForAll
}
// init free resource
func initFreeResource() {
for i := 0; i < cpuNum; i++ {
freeResource <- struct{}{}
}
}
//use free resource to do something
func useResource() {
for {
// use free resource
<-freeResource
go func() {
time.Sleep(time.Second * 1)
println("使用空闲资源做一些事:", atomic.AddInt64(&doneNum, 1))
// done something +1
done <- struct{}{}
}()
}
}
func releaseResource() {
for time.Now().Before(timeout2) {
//done 沒有数据会阻塞
<-done
// 资源数+1
freeResource <- struct{}{}
}
//超时 后
//waitForAll 有数据会阻塞
waitForAll <- struct{}{}
}
func main() {
RunScenario2()
}
输出
使用空闲资源做一些事: 1
使用空闲资源做一些事: 2
使用空闲资源做一些事: 3
使用空闲资源做一些事: 5
使用空闲资源做一些事: 7
使用空闲资源做一些事: 8
使用空闲资源做一些事: 4
使用空闲资源做一些事: 6
使用空闲资源做一些事: 9
使用空闲资源做一些事: 13
使用空闲资源做一些事: 12
使用空闲资源做一些事: 15
使用空闲资源做一些事: 16
使用空闲资源做一些事: 14
使用空闲资源做一些事: 11
使用空闲资源做一些事: 10
使用空闲资源做一些事: 17
使用空闲资源做一些事: 18
使用空闲资源做一些事: 19
使用空闲资源做一些事: 21
使用空闲资源做一些事: 24
使用空闲资源做一些事: 23
使用空闲资源做一些事: 20
使用空闲资源做一些事: 22
使用空闲资源做一些事: 25
使用空闲资源做一些事: 26
使用空闲资源做一些事: 27
使用空闲资源做一些事: 30
使用空闲资源做一些事: 31
使用空闲资源做一些事: 29
使用空闲资源做一些事: 32
使用空闲资源做一些事: 28
使用空闲资源做一些事: 33
使用空闲资源做一些事: 34
二、基于大数据量的并发模型
前面说的基于时间的并发模型,那如果只知道数据量很大,但是具体结束时间不确定,该怎么办呢?
比如,客户给了个几TB的文件列表,要求把这些文件从存储里删除。再比如,实现个爬虫去爬某些网站的所有内容。
而解决此类问题,最常见的就是使用工作池模式了(Worker Pool)。
以删文件为例,我们可以简单这样来处理
- Jobs 可以从文件列表里读取文件,初始化为任务,然后发给worker
- Worker 拿到任务开始做事
- Collector 收集worker处理后的结果
- Worker Pool 控制并发的数量
2.1、代码
package main
import (
"fmt"
"runtime"
"strconv"
"sync"
"time"
)
var cpuNum = runtime.NumCPU()
var taskTool = 5
var jobs = make(chan int, taskTool)
var results = make(chan string, taskTool)
var wg sync.WaitGroup
var totalTasks = 20 // 本例就要从文件列表里读取
func RunScenario3() {
for i := 0; i < cpuNum; i++ {
wg.Add(1)
//run task
go runTask(i, jobs, results, &wg)
}
wg.Add(1)
go showResult()
initJobs()
close(jobs)
wg.Wait()
}
func showResult() {
defer wg.Done()
for i := 0; i < totalTasks; i++ {
result := <-results
fmt.Println("任务结果:", result)
}
close(results)
}
func initJobs() {
for i := 0; i < totalTasks; i++ {
jobs <- i
}
}
//run task
func runTask(taskId int, jobs <-chan int, results chan<- string, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
res := "完成了任务--->job=" + strconv.Itoa(job) + ",taskId=" + strconv.Itoa(taskId)
fmt.Println("Worker run task", taskId, res)
time.Sleep(time.Millisecond * time.Duration(100))
results <- res
}
}
func main() {
RunScenario3()
}
输出
Worker run task 3 完成了任务--->job=3,taskId=3
Worker run task 7 完成了任务--->job=4,taskId=7
Worker run task 4 完成了任务--->job=5,taskId=4
Worker run task 5 完成了任务--->job=7,taskId=5
Worker run task 6 完成了任务--->job=6,taskId=6
Worker run task 0 完成了任务--->job=0,taskId=0
Worker run task 1 完成了任务--->job=1,taskId=1
Worker run task 2 完成了任务--->job=2,taskId=2
Worker run task 0 完成了任务--->job=8,taskId=0
Worker run task 5 完成了任务--->job=11,taskId=5
Worker run task 4 完成了任务--->job=10,taskId=4
任务结果: 完成了任务--->job=0,taskId=0
任务结果: 完成了任务--->job=4,taskId=7
任务结果: 完成了任务--->job=5,taskId=4
Worker run task 6 完成了任务--->job=12,taskId=6
任务结果: 完成了任务--->job=7,taskId=5
任务结果: 完成了任务--->job=6,taskId=6
任务结果: 完成了任务--->job=3,taskId=3
Worker run task 7 完成了任务--->job=9,taskId=7
Worker run task 1 完成了任务--->job=15,taskId=1
任务结果: 完成了任务--->job=2,taskId=2
任务结果: 完成了任务--->job=1,taskId=1
Worker run task 2 完成了任务--->job=14,taskId=2
Worker run task 3 完成了任务--->job=13,taskId=3
Worker run task 3 完成了任务--->job=16,taskId=3
Worker run task 1 完成了任务--->job=17,taskId=1
Worker run task 7 完成了任务--->job=19,taskId=7
Worker run task 2 完成了任务--->job=18,taskId=2
任务结果: 完成了任务--->job=13,taskId=3
任务结果: 完成了任务--->job=15,taskId=1
任务结果: 完成了任务--->job=14,taskId=2
任务结果: 完成了任务--->job=9,taskId=7
任务结果: 完成了任务--->job=12,taskId=6
任务结果: 完成了任务--->job=8,taskId=0
任务结果: 完成了任务--->job=10,taskId=4
任务结果: 完成了任务--->job=11,taskId=5
任务结果: 完成了任务--->job=19,taskId=7
任务结果: 完成了任务--->job=16,taskId=3
任务结果: 完成了任务--->job=18,taskId=2
任务结果: 完成了任务--->job=17,taskId=1
在Go里,分发任务,收集结果,我们可以都交给Channel来实现。从实现上更加的简洁。
仔细看会发现,本模型也是适用于按时间来控制并发。只要把totalTask的遍历换成时间控制就好了。
三、等待异步任务执行结果
3.1、代码
package main
import (
"fmt"
"math/rand"
"time"
)
func RunScenario4() {
input := make(chan int)
output := make(chan int)
go func() {
id := rand.Intn(100)
for {
input <- id
}
}()
go func() {
for {
output <- <-input
}
}()
select {
case out := <-output:
fmt.Println("output:", out)
case <-time.After(time.Duration(30 * time.Second)):
fmt.Errorf("指定时间内都没有得到结果")
default:
fmt.Errorf("默认报错")
}
}
func main() {
RunScenario4()
}
输出
output: 81
在select的case情况,加上time.After()模型可以让我们在一定时间范围内等待异步任务结果,防止程序卡死。
四、定时反馈异步任务结果
上面我们说到持续的压测某后端API,但并未实时收集结果。而很多时候对于性能测试场景,实时的统计吞吐率,成功率是非常有必要的。
4.1、代码
package main
import (
"fmt"
"time"
)
func RunScenario5() {
go func() {
for {
time.Sleep(time.Second * 1)
fmt.Println("do something")
}
}()
t := time.NewTicker(time.Second * 3)
for {
select {
case <-t.C:
// 计算并打印实时数据
fmt.Println("每隔3秒获取结果数据")
}
}
}
func main() {
RunScenario5()
}
输出
do something
do something
每隔3秒获取结果数据
do something
do something
do something
每隔3秒获取结果数据
do something
do something
do something
每隔3秒获取结果数据
do something
do something
do something
每隔3秒获取结果数据
do something
do something