go并发控制--WaitGroup 和 Select-case

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 中写入数据时,就可以对其进行处理。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容