在日常工作中,使用协程提高处理效率是非常常见的技巧,这时候我们需要同时执行多个协程,然后再根据其结果再进行处理,这时候收集多个协程的值就非常关键。
相信稍有经验的程序员都能写出如下代码
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
ch := make(chan int)
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Goroutine %d is working\n", id)
ch <- id // Send a signal to the channel when done
}(i)
}
// Wait for all Goroutines to signal completion
go func() {
wg.Wait()
close(ch) // Close the channel when all Goroutines are done
}()
for id := range ch {
fmt.Printf("Received signal from Goroutine %d\n", id)
}
fmt.Println("All Goroutines have finished.")
}
在这个示例中,我们有三个 Goroutines
执行工作,并往一个通道ch
中发送信息。而另一个独立的 Goroutine
则监听通道,使用 sync.WaitGroup
来等待所有 Goroutines
完成,以知道每个 Goroutine
何时完成其工作从而关闭ch
。
当然,大家更常见的写法可能是下面这样,区别在于sync.WaitGroup
与close
操作不单独开启协程,而是同步等待。
package main
import (
"fmt"
"sync"
)
func worker(id int, resultChan chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
result := id * 2
resultChan <- result // Send the result to the channel
}
func main() {
var wg sync.WaitGroup
resultChan := make(chan int, 3)
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, resultChan, &wg)
}
wg.Wait() // Wait for all workers to finish
close(resultChan) // Close the channel when all results are sent
for result := range resultChan {
fmt.Printf("Received result: %d\n", result)
}
}
在这个示例中,三个工作 Goroutines
计算结果并将它们发送到一个通道。主 Goroutine
等待所有工作者完成,关闭通道,然后从通道中读取和处理结果。
这些示例说明了如何使用通道同步在 Go
中的各种并发编程场景中协调和同步 Goroutines
。通道为 Goroutines
之间提供了一个强大的机制,使得编写行为可预测和可靠的并发程序变得更加容易。
假如有这样一个场景,我们需要对一批用户中的每个用户执行某个操作,比如获取用户当月积分,需要所有用户都执行成功才算成功,有一个执行失败就应该整体返回错误(有点类似事务了,但实际场景不太一样,事务是多个操作要么都成功,要么都失败,而我这里例举的操作,是对同一批主体执行同一个操作)
// 定义结构体,用于保存每个主体的执行结果
type bizResult struct {
ExecFailed bool
ErrMsg string
Value int64
}
func GetUserScore(userId int64) (score int64, err error) {
// 假设涉及到一些查DB以及RPC调用等,可能还涉及到一些复杂计算,这为了简便直接写死100
res := int64(100)
return res,nil
}
func GetUserScores(userIds []int64) (scores int64, err error) {
var totalScores int64
bizResultChan := make(chan *bizResult, len(userIds))
var wg sync.WaitGroup
for _, userId := range userIds {
wg.Add(1)
go func(userId int64) {
defer wg.Done()
score, err := GetUserScore(userId)
if err != nil {
bizResultChan <- &bizResult{
ExecFailed: true,
ErrMsg: fmt.Sprintf("get user score failed, userId: %v, err: %v", userId, err),
}
return
}
bizResultChan <- &bizResult{
ExecFailed: false,
Value: score,
}
}(userId)
}
wg.Wait()
close(bizResultChan)
for brc := range bizResultChan {
if brc == nil {
continue
}
if brc.ExecFailed {
msg := fmt.Sprintf("[GetUserScores] get GetUserScore failed, err: %v", brc.ErrMsg)
return 0,fmt.Errorf(msg)
}
totalScores += brc.Value
}
return totalScores,nil
}
首先,假设我们有一个需求:接口调用时,接收到一个包含十个元素的列表。我们希望并发执行这十个任务,每个任务都会返回执行的结果和可能的异常。最后,我们要将返回的结果整合到一个切片列表中,然后一并返回。
为了实现这个需求,我们首先定义了一个结构体 Order 用于表示任务的信息:
type Order struct {
Name string `json:"name"`
Id int `json:"id"`
}
然后,我们决定并发执行十个任务,因此初始化了两个通道,一个用于接收任务的结果,另一个用于接收异常:
taskNum := 10
orderCh := make(chan Order, taskNum) // 用于接收返回的结果
errCh := make(chan error, taskNum) // 用于接收返回的异常
接下来,我们创建了任务执行函数:
func processTask(task Task) {
// 执行任务的逻辑
}
然后,我们启动了十个协程来执行这些任务,并使用 sync.WaitGroup
来等待它们完成:
var wg sync.WaitGroup
for i := 0; i < taskNum; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 任务的执行逻辑
processTask(task)// 注意这里task指参数,是伪代码
}()
}
// 等待所有任务协程完成
wg.Wait()
接着,我们使用 for-select
结构从结果通道中接收执行结果:
orderList := make([]Order, taskNum)
for i := 0; i < taskNum; i++ {
select {
case order, ok := <-orderCh:
if ok {
orderList = append(orderList, order)
}
case err := <-errCh:
if err != nil {
return err // 在发现错误时,根据需求选择是继续执行还是返回错误
}
default:
fmt.Println("done")
}
}
最后,我们关闭了通道,表示不再发送任务:
close(orderCh)
close(errCh)
如果我们需要控制每个任务的执行时间,我们可以使用定时器来解决超时问题:
timeoutTime := time.Second * 3 // 超时时间
taskTimer := time.NewTimer(timeoutTime) // 初始化定时器
for i := 0; i < taskNum; i++ {
select {
case <-taskTimer.C:
err := errors.New("task timeout")
return err
// 其他 case 分支处理任务的执行和结果接收
}
// 每次执行都需要重置定时器
taskTimer.Reset(timeoutTime)
}
在协程内处理 panic
问题是很重要的,我们在协程内使用 defer
来捕获 panic
:
for i := 0; i < taskNum; i++ {
wg.Add(1)
go func() {
defer func() {
wg.Done()
if r := recover(); r != nil {
err := errors.New(fmt.Sprintf("System panic: %v", r))
errCh <- err
return
}
}()
// 任务的执行逻辑
processTask(task)// 注意这里task指参数,是伪代码
}()
}
最后,如果我们需要保持任务执行结果的顺序,我们可以定义一个带序号的结构体,并通过带序号的通道接收结果:
type OrderWithSeq struct {
Seq int
OrderItem Order
}
orderCh := make(chan OrderWithSeq, taskNum) // 用于接收带序号的结构体
在任务执行时,我们加入序号信息:
for i := 0; i < taskNum; i++ {
i := i
wg.Add(1)
go func(i int) {
defer wg.Done()
// 任务的执行逻辑
// 注意这里task指参数,是伪代码
// res指返回值,也是伪代码,实际processTask应该还会返回err
res := processTask(task)
orderCh <- OrderWithSeq{
Seq: i,
OrderItem: res,
}
}(i)
}
最后,在结果接收时,按照带序号的结构体进行排序:
orderSeqList := make([]OrderWithSeq, taskNum)
for i := 0; i < taskNum; i++ {
select {
case order, ok := <-orderCh:
if ok {
orderList = append(orderSeqList, order)
}
// 其他 case 分支处理异常等
}
}
// 按原始顺序进行排序
sort.Sort(BySeq(orderSeqList))
这就是一个完整的批量执行任务的通用模板,注意它是伪代码,主要提供的是思路,根据实际需求和场景,我们需要调整其中的一些部分。