Q:假如现在需要实现一个API,该API会执行多个 python 脚本,并根据爬取结果进行数据处理。
A:比较容易想到我们需要使用go并发控制,以下是两种处理方式
使用 waitGroup
tasks := []taskStruct{taskA, taskB, taskC}
var wg sync.WaitGroup
wg.Add(len(tasks))
res := []taskResult{}
for i := 0; i < len(tasks); i++ {
go func(id uint) {
defer wg.Done()
r, err := executePythonScript(id)
if err != nil {
// log and do something
return
}
res := append(res, r)
}(tasks[i].id)
}
wg.Wait()
for i := range res {
// deal with result
}
使用 Select-case
tasks := []taskStruct{taskA, taskB, taskC}
ch := make(chan taskResult)
for i := 0; i < len(tasks); i++ {
go func(id uint) {
res, err := executePythonScript(id)
if err != nil {
// log and return
return
}
ch <- taskResult{
id: res.id,
content: res.content,
errors: res.errors,
}
}(tasks[i].id)
}
for i := 0; i < len(tasks); i++ {
select {
case t := <- ch:
// do something
case <- time.After(5 * time.Minute):
// do something
}
}
对比
-
流程比较
- 使用 waitGroup 的例子需要等待 所有待执行的脚本 全部执行完之后,在对每一个结果进行其他操作
- 使用 Select-case ,程序先是被 for-loop 里的 select-case 所阻塞(读写值为nil的channel),当协程里每执行完一个脚本得到结果后就会向 channel 中写入数据,for-loop 里的 select-case 读到 channel 中的值进行相关操作且进入下一层循环。
-
线程安全
- 使用 WaitGroup 的例子是比较容易想到,易于实现的,即每一个子协程执行完脚本后都会向 slice 中添加结果,当所有子协程结束后在进行操作。但是在go语言中使用 slice、map等共享内存的数据结构来存储每一个协程的处理结果,这样的写法不是线程安全的。
- 在使用 Select-case 的例子中,使用 Channel 传递数据,即每个子协程得到结果后通过 channel 向主协程传递结果,并且保证了线程安全。
Go 语言中最常见的、也是经常被人提及的设计模式就是:不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存。
-
优略对比
两种并发控制模式各有优略,结合例子来说的话就是,WaitGroup适合等待一系列子协程完成操作,也就是等待一个最终结果。而 Select-case + channel 比较适合无需关注全部子协程的状态,当某个子协程向 channel 中写入数据时,就可以对其进行处理。