Go 退出通知机制

370 阅读6分钟

Go 退出通知机制

写这篇文章灵来源自 并发的退出 - Go语言圣经

所以本文大概是个学习笔记。

Go语言并没有提供在一个 goroutine 中终止另一个 goroutine 的方法,由于这样会导致 goroutine 之间的共享变量落在未定义的状态上。

在 Go 语言中,我们通常使用并发编程来实现高效的并发处理。在这种情况下,我们通常需要使用一些机制来协调不同 goroutine 之间的工作。其中一种常见的机制是退出通知机制,也称为取消机制。在本文中,我们将介绍什么是退出通知机制,以及如何在 Go 中实现它。

什么是退出通知机制

退出通知机制是一种在多个 goroutine 之间进行同步和通信的常见方式。它通常用于在程序的某个地方关闭一个通道(也称为退出通道或取消通道),以通知其他 goroutine 停止运行。然后,在每个 goroutine 中,我们可以使用一个函数来检查该通道是否已经被关闭,以决定是否需要终止当前的 goroutine。

在 Go 语言中,我们通常使用 select 语句来监听多个通道,以实现退出通知机制。具体来说,我们可以定义一个名为 done 的通道,并在程序的某个地方使用 close(done) 来关闭该通道。然后,在每个 goroutine 中,我们可以使用一个函数来检查 done 通道是否已经被关闭,以决定是否需要终止当前的 goroutine。这个函数通常被称为 cancelled(),具体实现如下:

func cancelled() bool {
    select {
    case <-done:
        return true
    default:
        return false
    }
}

在这个函数中,我们使用了一个 select 语句来监听 done 通道。如果 done 通道已经被关闭,那么 case <-done: 分支会被选择,并返回 true。否则,default: 分支会被选择,并返回 false

补充:为什么会这么选择

1. select 语句的执行原理大致如下:

  1. 首先,select 语句会创建一个空的通信选择器。
  2. 然后,select 语句会依次检查每个 case 分支,从中选择一个可执行的分支。如果多个分支同时可执行,则会随机选择一个分支。
  3. 如果找到了一个可执行的分支,则执行该分支中的语句,并退出 select 语句。如果所有分支都不可执行,则进入阻塞状态,等待其中一个分支变为可执行。
  4. 当一个或多个分支变为可执行时,select 语句会重新检查所有分支,并选择其中一个可执行的分支。如果多个分支同时可执行,则会随机选择一个分支,并执行该分支中的语句。
  5. 重复执行步骤 4,直到 select 语句退出或被中断。

2. 为什么 done 通道关闭后会选择 case: <-done 分支:

如果某个通道已经被关闭,那么该通道会一直处于可读状态,即每次读取该通道都会返回其所传输的零值。即当一个通道被关闭时,该通道的读取操作会立即返回该通道的零值,而不会阻塞等待数据的到来。

具体来说,当 done 通道被关闭时,select 语句会立即 done 通道中读取一个零值,并选择该通道的 case 分支执行。

3. 为什么通道没有关闭时不选择 case: <-done 分支:

在代码中使用 select 语句监听一个未关闭的通道时,如果该通道没有数据可读,则会进入阻塞状态,等待该通道有数据可读或该通道被关闭。

为什么使用退出通知机制

退出通知机制是一种在多个 goroutine 之间进行同步和通信的常见方式,它可以帮助我们实现以下目标:

  1. 安全退出:我们可以使用退出通知机制来安全地停止正在运行的 goroutine,以避免死锁或其他意外情况的发生。
  2. 资源释放:我们可以使用退出通知机制来释放正在使用的资源,以避免内存泄漏或其他资源泄漏的发生。
  3. 防止僵尸 goroutine:如果一个 goroutine 在完成工作后不及时退出,那么它将成为一个“僵尸 goroutine”,占用系统资源并可能导致其他问题的发生。使用退出通知机制,我们可以及时停止这些“僵尸 goroutine”。

如何实现退出通知机制

在 Go 语言中,实现退出通知机制可以分为以下几个步骤:

  1. 定义一个退出通道:我们可以定义一个名为 done 的通道,并使用 close(done) 来关闭该通道,以通知其他 goroutine 停止运行。
  2. 在每个 goroutine 中使用 select 语句监听 done 通道:我们可以在每个 goroutine 中使用一个函数来检查 done 通道是否已经被关闭,以决定是否需要终止当前的 goroutine。这个函数通常被称为 cancelled(),具体实现可以参考前面的代码示例。
  3. 在程序的适当位置关闭 done 通道:我们需要在程序的适当位置使用 close(done) 来关闭 done 通道,以通知所有监听该通道的 goroutine 停止运行。

示例代码

package main

import (
	"fmt"
	"sync"
	"time"
)

func cancelled(done <-chan struct{}) bool {
	select {
	case <-done:
		return true
	default:
		return false
	}
}

// 定义一个名为 worker 的函数,接收三个参数:
// id 表示当前 goroutine 的编号
// wg 是一个 *sync.WaitGroup 类型的指针,用于等待所有 goroutine 完成
// stopCh 是一个 <-chan struct{} 类型的通道,用于接收退出通知
func worker(id int, wg *sync.WaitGroup, done <-chan struct{}) {
	// 在函数结束时调用 wg.Done(),标记一个 goroutine 已经完成
	defer wg.Done()
	// 在函数开始时打印一条消息,指示该 goroutine 已经开始
	fmt.Printf("worker %d: started\n", id)
	// 在函数结束时打印一条消息,指示该 goroutine 已经退出
	defer fmt.Printf("worker %d: exited\n", id)

	// 在取消事件之后创建的 goroutine 改变为无操作
	if cancelled(done) {
		return
	}

	// 使用一个无限循环来模拟一些工作
	for {
		select {
		// 监听 done 通道,如果通道被关闭,则立即返回
		case <-done:
			return
		default:
			fmt.Printf("worker %d: working...\n", id)
			time.Sleep(500 * time.Millisecond)
		}
	}
}

func main() {
	// 创建一个 done 通道,用于发送退出通知
	done := make(chan struct{})
	// 创建一个 sync.WaitGroup 类型的变量 wg,用于等待所有 goroutine 完成
	var wg sync.WaitGroup
	// 启动 5 个 goroutine,并在后台运行 worker 函数
	for i := 0; i < 5; i++ {
		// 在循环中,每个 goroutine 都需要增加 wg 的计数器
		wg.Add(1)
		go worker(i, &wg, done)
	}

	// 模拟一些工作,让程序运行一段时间
	time.Sleep(2 * time.Second)

	// 关闭 done 通道,向所有 goroutine 发送退出通知
	close(done)
	// 启动一个新的 goroutine 运行 worker 函数
	wg.Add(1)
	go worker(5, &wg, done)
	// 等待所有 goroutine 完成
	wg.Wait()

	fmt.Println("all workers stopped")
}