Goroutine 互相控制 demo
Goroutine 是 Go 语言中用于实现并发执行的轻量级线程。在工作中,我们很可能遇见着父子 Goroutine 互相控制的情况。
父子 goroutine 之间重要的交集 —— 生命周期管理
- 互不影响,互相独立
- 父控制子(父 goroutine 结束时,子 goroutine 也随之结束)
- 子控制父(子 goroutine 结束时,父 goroutine 也随之结束)
互不影响
func Parent() {
go Child()
}
func Child() {
}
父控制子
func Parent() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go Child(ctx)
}
func Child(ctx context.Context) {
select {
case <-ctx.Done():
// parent 完成后退出
return
}
}
也可以使用 channel 来通知子 goroutine 关闭,但是优先使用 context。上面这段代码只起到通知的作用,具体关闭的逻辑需要由子 goroutine 自己实现,没有需要回收的资源时可以直接 return 或者 runtime.Goexit() 关闭子 goroutine。
子控制父 (一对一)
func Parent() {
ch := make(chan struct{})
go Child(ch)
select {
// 获取通知并退出
case <-ch:
return
}
}
func Child(ch chan<- struct{}) {
// 通知 父 goroutine 的 channel
ch <- struct{}{}
}
父 goroutine 创建了一个通道,并启动了一个子 goroutine。子 goroutine 执行完工作后,向通道发送一个值。父 goroutine 使用 select 语句监听通道事件,并在接收到子 goroutine 发送的空结构体通知后退出。
子控制父 (多对一)
有时一个父 goroutine 创建了 n 个子 goroutine,可能需要n个子 goroutine 都结束或者n个子 goroutine 中的 m个结束时停止父 goroutine。
所有子 goroutine 都结束后对控制父 goroutine 操作用 sync.WaitGroup 或者 errorgroup 很容易实现,此处省略
n个子 goroutine中的m个结束时停止父 goroutine:
func parent(n, m int) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch := make(chan struct{})
go func() {
for i := 0; i < n; i++ {
i := i
go child(ctx, ch, i)
}
}()
count := 0
for {
<-ch
count++
if count == m {
cancel()
break
}
}
fmt.Println("父 goroutine 结束")
}
func child(ctx context.Context, ch chan<- struct{}, i int) {
select {
case <-ctx.Done():
fmt.Printf("子 goroutine %d 取消执行任务\n", i)
return
default:
fmt.Printf("子 goroutine %d 正在执行\n", i)
ch <- struct{}{}
}
}
子 goroutine 执行完成后给往父 goroutine 监听的管道中发送一个信号,父 goroutine 判断满足条件就退出。已经被创建出来开始工作的子 goroutine 无法在工作过程中取消工作,如果是循环进行相同的工作或者是定时任务,可以 for - select 监听 context 撤销然后退出。
多协程查切片问题
假设有一个超长的切片,切片的元素类型为int,切片中的元素为乱序排序。限时5秒,使用多个 goroutine 查找切片中是否存在给定的值,在查找到目标值或者超时后立刻结束所有 goroutine 的执行。
比如,切片 [23,32,78,43,76,65,345,762,......915,86],查找目标值为 345 ,如果切片中存在,则目标值输出对应的索引,并立即取消仍在执行查询任务的goroutine。
如果在超时时间未查到目标值程序,则输出超时提示,同时立即取消仍在执行的查找任务的goroutine。
func main() {
rand.Seed(time.Now().UnixNano())
// 创建切片
slice := make([]int, 66)
// 生成随机初始值
for i := 0; i < len(slice); i++ {
slice[i] = rand.Intn(60)
}
fmt.Printf("原始切片为:%+v\n,长度为:%d\n", slice, len(slice))
v := 54 // 要查找的目标值
ctx, cancel := context.WithCancel(context.Background())
result := make(chan int)
go FindNum(ctx, slice, v, result)
timer := time.NewTimer(time.Second * 5)
for {
select {
case <-timer.C:
fmt.Println("5s 内未查到结果,超时终止")
cancel()
time.Sleep(3 * time.Second)
return
case res := <-result:
fmt.Printf("查询目标的索引为:%d\n", res)
cancel() // 其他 goroutine 已经查到结果,通知全部终止
time.Sleep(3 * time.Second)
return
}
}
}
func FindNum(ctx context.Context, slice []int, value int, res chan<- int) {
lenSlice := len(slice)
n := int(math.Ceil(float64(lenSlice) / 10.0))
for i := 0; i < n; i++ {
go func(ctx context.Context, times int) {
for j := n * times; j < n*(times+1); j++ {
select {
case <-ctx.Done():
fmt.Printf("goroutine %d 查询终止\n", times)
return
default:
time.Sleep(time.Second)
if j >= lenSlice {
return
}
if slice[j] == value {
res <- j
return
}
}
}
}(ctx, i)
}
}