Goroutine 互相控制 demo

44 阅读3分钟

Goroutine 互相控制 demo

​ Goroutine 是 Go 语言中用于实现并发执行的轻量级线程。在工作中,我们很可能遇见着父子 Goroutine 互相控制的情况。

父子 goroutine 之间重要的交集 —— 生命周期管理

  1. 互不影响,互相独立
  2. 父控制子(父 goroutine 结束时,子 goroutine 也随之结束)
  3. 子控制父(子 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)
	}
}