select 阻塞

在线运行open in new window启动 AI 助手open in new window

func main() {
	// fatal error: all goroutines are asleep - deadlock!
	// select {}

	ch := make(chan int)

	select {
	case v, ok := <-ch:
		// 因为 ch 没有机会写入,所以改 case 永远都是阻塞的
		fmt.Println(ok, v)
	default:
		fmt.Println("always here")
	}
}

在程序中,首先使用 make() 函数创建了一个整型通道 ch。然后,程序使用了 select 语句对 ch 进行读取操作。由于 ch 中没有任何值,所以程序不会阻塞在 select 语句中的 case 中等待通道有值,而是立即执行了 default 语句块,输出一条消息 "always here"。

需要注意的是,程序中的 select 语句只有一个 case,而且是一个非阻塞的 case,这样做的目的是为了避免 select 语句一直阻塞在某个 case 中等待通道有值,从而导致程序死锁。如果将程序中的 select 语句修改为只有一个 case 且不是非阻塞的 case,那么程序将会发生死锁。

使用 default 让通道不阻塞

在线运行open in new window启动 AI 助手open in new window

func main() {
	ch := make(chan int)

	for i := 0; i < 10; i++ {
		select {
		case i := <-ch:
			// 没有写入的机会,永远是阻塞的
			fmt.Println(i)

		default:
			fmt.Println("default")
			// 如果没有 default 分支程序会报:
			// fatal error: all goroutines are asleep - deadlock!
		}
	}

	// 输出 10 行 default
}

在程序中,首先使用 make() 函数创建了一个整型通道 ch。然后,程序使用了 for 循环迭代 10 次,并在每次迭代中使用 select 语句对 ch 进行读取操作。由于 ch 中没有任何值,所以程序不会阻塞在 select 语句中的 case 中等待通道有值,而是立即执行了 default 语句块,输出一条消息 "default"。这样,程序一共输出了 10 行 "default" 消息。

需要注意的是,在程序中的 select 语句中,仅仅只有一个非阻塞的 default 分支,这样做的目的是为了避免 select 语句一直阻塞在某个 case 中等待通道有值,从而导致程序死锁。由于通道中没有值可供读取,所以在每次迭代中都会执行 default 分支,从而避免了程序的阻塞。

select 分支的随机性

有缓冲版本open in new window启动 AI 助手open in new window

func main() {
	ch1 := make(chan int, 1)
	ch2 := make(chan int, 1)

	ch1 <- 1
	ch2 <- 1

	select {
	case <-ch1:
		fmt.Println("got ch1")
	case <-ch2:
		fmt.Println("got ch2")
	}

	// 可能输出 ch1 也可能输出 ch2
}

在程序中,首先使用 make() 函数创建了两个整型通道 ch1 和 ch2,并向这两个通道中分别写入了值 1。然后,程序使用了 select 语句对 ch1 和 ch2 进行读取操作。由于 ch1 和 ch2 中都有值,所以程序会随机选择一个 case 执行,即有可能输出 "got ch1",也有可能输出 "got ch2"。

需要注意的是,在程序中的 select 语句中,两个 case 的操作都是非阻塞的,这是因为通道中已经有值,所以在执行 select 语句时,程序不需要等待通道中有值。如果在 select 语句中使用了阻塞的 case,那么程序可能会一直等待某个通道有值,从而导致程序死锁。

无缓冲版本open in new window启动 AI 助手open in new window

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		ch1 <- 1
		time.Sleep(1 * time.Second)
	}()

	go func() {
		ch2 <- 1
		time.Sleep(1 * time.Second)
	}()

	time.Sleep(1 * time.Second)

	select {
	case <-ch1:
		fmt.Println("got ch1")
	case <-ch2:
		fmt.Println("got ch2")
	}

	// 可能输出 ch1 也可能输出 ch2
}

在程序中,首先使用 make() 函数创建了两个整型通道 ch1 和 ch2。然后,程序启动了两个协程,分别向 ch1 和 ch2 中写入值,并在写入后等待 1 秒钟。接着,程序在主线程中等待 1 秒钟,以确保两个协程中的写操作都已经完成。最后,程序使用了 select 语句对 ch1 和 ch2 进行读取操作。由于 ch1 和 ch2 中都有值,所以程序会随机选择一个 case 执行,即有可能输出 "got ch1",也有可能输出 "got ch2"。

需要注意的是,在程序中的协程中,程序使用了 time.Sleep() 函数模拟了一些耗时操作,这是为了让程序的运行更加明显。如果没有这些耗时操作,程序可能会很快结束,而不需要等待 1 秒钟。另外,在协程中,程序使用了通道来进行协程之间的通信,这是 Go 编程语言中常用的一种方式,可以避免在协程之间共享变量所导致的线程安全问题。

select 中通道不限于读取也可写入

在线运行open in new window启动 AI 助手open in new window

func main() {
	ch := make(chan int, 1)

	for i := 0; i < 10; i++ {
		// 写入一个,有随机的效果,注意并不是交替执行
		select {
		case ch <- 0:
		case ch <- 1:
		}

		i := <-ch
		fmt.Println("value received:", i)
	}

	/*
		value received: 0
		value received: 1
		value received: 1
		value received: 0
		value received: 1
		value received: 0
		value received: 1
		value received: 0
		value received: 0
		value received: 0
	*/
}

在程序中,首先使用 make() 函数创建了一个整型通道 ch,并指定其缓冲区大小为 1。然后,程序使用了 for 循环执行了 10 次以下操作:向 ch 中写入值 0 或 1,然后从 ch 中读取值并打印。由于通道的缓冲区大小为 1,因此写入操作不会阻塞程序,即使多个协程同时写入通道,也只有一个值能够被缓存。程序使用 select 语句实现了随机写入 0 或 1 的功能。

需要注意的是,在程序中的 select 语句中的 case 是非阻塞的,因为通道中的值在执行 select 语句时已经准备好了。如果通道中没有值,那么 select 语句会阻塞程序,直到有值可以读取或写入。另外,由于 select 语句的执行是随机的,因此在程序中读取的值也是随机的,不一定是按照写入的顺序进行的。

使用 ok 范式判断关闭的 chan

在线运行open in new window启动 AI 助手open in new window

func main() {
	ch1 := make(chan int, 1)
	ch2 := make(chan int, 1)

	close(ch1)

	ch2 <- 1

	select {
	case _, ok := <-ch1:
		fmt.Println("got ch1", ok)
	case v, ok := <-ch2:
		fmt.Println("got ch2", v, ok)
	}

	// 可能输出 got ch1 false
	// 也可能输出 got ch2 1 true
}

在程序中,首先使用 make() 函数创建了两个整型通道 ch1 和 ch2,并指定它们的缓冲区大小都为 1。然后,程序使用了 close() 函数关闭了 ch1 通道,使得从 ch1 通道读取值时可以判断通道是否已关闭。

程序接着使用了 select 语句从 ch1 和 ch2 中读取值。由于 ch1 已经被关闭,因此从 ch1 中读取值时,可以判断通道是否已关闭。如果 ch1 通道已关闭,则第一个 case 分支将被执行,输出 "got ch1 false"。如果 ch1 通道未关闭,则会尝试从 ch2 通道中读取值。如果 ch2 中有值,第二个 case 分支将被执行,输出 "got ch2 1 true",其中 1 是从 ch2 中读取的值,true 表示通道未关闭。如果 ch2 中没有值,则程序将一直等待,直到 ch2 中有值为止。

需要注意的是,在程序中的 select 语句中的 case 是非阻塞的,因为通道中的值在执行 select 语句时已经准备好了。如果通道中没有值,那么 select 语句会阻塞程序,直到有值可以读取或写入。如果通道已经关闭,那么从通道中读取值时会返回通道元素类型的零值,并且第二个返回值会被设置为 false。

使用 select 来实现超时控制

在线运行open in new window启动 AI 助手open in new window

func main() {
	c := make(chan int)

	go func() {
        // 模拟线程运行的时间
		time.Sleep(2 * time.Second)
		c <- 1
	}()

	select {
	case num := <-c:
		fmt.Println(num)
	case <-time.After(time.Second * 1):
        // 如果线程超过1秒就超时了
		fmt.Println("timeout")
	}

	// timeout
}

在程序中,首先使用 make() 函数创建了一个整型通道 c,然后使用 go 关键字开启了一个新的 Goroutine。在新的 Goroutine 中,程序模拟了线程运行的时间,即睡眠了 2 秒钟,然后向通道 c 中写入了值 1。

接着,程序使用了 select 语句从通道 c 中读取值。在 select 语句中,第一个 case 分支尝试从通道 c 中读取值,并将读取到的值赋值给 num 变量。第二个 case 分支使用了 time.After() 函数创建了一个定时器,并设置了超时时间为 1 秒钟。如果从通道 c 中读取值的时间超过了 1 秒钟,即 Goroutine 的执行时间超过了 1 秒钟,那么第二个 case 分支将被执行,输出 "timeout"。

由于在新的 Goroutine 中睡眠了 2 秒钟,所以从通道 c 中读取值的时间超过了 1 秒钟,第二个 case 分支将被执行,程序输出 "timeout"。需要注意的是,如果 Goroutine 的执行时间少于 1 秒钟,第一个 case 分支将被执行,程序将输出从通道 c 中读取的值。

使用 select 实现斐波那契数列

在线运行open in new window启动 AI 助手open in new window

func fib(ch chan int, max int) {
	x, y := 0, 1

	for {
		select {
		case ch <- x:
			x, y = y, x+y
			if x > max {
				// 注意这里 return 直接结束函数,不是 break
				return
			}
		default:
			// default 分支不是必须得
		}
	}
}

func main() {
	// 此例中 ch 是无缓冲、有缓冲通道都行
	// ch := make(chan int)
	ch := make(chan int, 1)
	exit := make(chan struct{})

	go func() {
		// 通道 ch 没有关闭前要么读取到数据要么阻塞
		for n := range ch {
			fmt.Println(n)
		}

		// 确保打印完通道的数据再结束线程
		<-exit
	}()

	fib(ch, 100)

	// 确保 range 通道正常退出
	close(ch)
	exit <- struct{}{}
}

程序实现了一个斐波那契数列的生成器。程序中定义了一个名为 fib 的函数,该函数使用了 Go 的 select 机制和协程,实现了生成斐波那契数列的功能。main 函数中启动了一个协程,使用无缓冲或有缓冲通道来读取 fib 函数生成的斐波那契数列,读取完毕后关闭通道并退出线程。

fib 函数使用 select 机制,通过无限循环向通道中发送斐波那契数列的元素,同时不阻塞 select 机制,如果通道中已有元素则直接跳到 default 分支。main 函数中启动一个协程来读取 fib 函数生成的斐波那契数列,通过 range 关键字遍历通道中的所有元素,直到通道被关闭。确保 main 函数在通道被关闭后再退出,避免通道中的数据还未完全读取就退出线程的情况。

需要注意的是,程序中使用了两个协程来实现并发,可以通过使用有缓冲通道来控制输出速率,避免协程阻塞的情况。同时,fib 函数通过 return 直接结束函数,而不是使用 break 跳出循环,这是因为 select 机制并不能直接结束函数。

Last Updated:
Contributors: Bob Wang