GoLang channel之夜话

624 阅读6分钟

关键词

channel、阻塞、panicselect

导语

ChannelGolang语言的特色,用于多个Goroutine之间的通信。本文首先探讨对其发送、接收操作的特点,然后讨论什么时候channel会阻塞、什么时候channelpanic,最后讨论专门为channel而生的select语句

channel通道发送与接收操作的特点

  • 发送操作与接收操作都是互斥的

解释:多个goroutine中同时向同一个channel发送或者接收时,只能一个goroutinechannel进行发送或接收,发送之间互斥,接收之间也互斥。

  • 发送操作在完全完成之前会被阻塞,接收操作也是如此。
  • 发送操作和接收操作中对元素值的处理都是不可分割的。 解释: 发送操作,不是直接将值将入到channel,而是第一步、进行复制,第二步、将副本加入到channel中,这个过程不可分割且会阻塞。 接收操作,不是直接将值直接赋值给变量,而是第一步、进行复制,第二步、将副本赋值给变量,第三部、删除channel中的相应元素。这个过程不可分割且会阻塞。

对通道的发送与接收操作,什么时候会阻塞?

  • 通道为nil时,发送与接收操作全都会阻塞

解释:通道channel为引用类型,只声明不用make初始化,此时channel就是一个nil

package main

import (
	"fmt"
	"time"
)

func main()  {
	var ch chan int
	go func() {
		ch <- 1
		fmt.Println("goroutine finish")
	}()
	time.Sleep(10*time.Second)
}
// main goroutine 会等待10s,不会打印goroutine finish,说明一直阻塞
  • 缓冲通道,已满,进行发送操作,发送操作阻塞
package main

import (
	"fmt"
	"time"
)

func main() {
	var ch = make(chan int, 3)
	ch <- 1
	ch <- 2
	ch <- 3
	fmt.Println("channel is fill")
	go func() {
		ch <- 4
		fmt.Println("fill channel can  receive a number?")
	}()
	time.Sleep(5 * time.Second)
}
  • 缓冲通道,已空,进行接收操作,接收操作阻塞
  • 非缓冲通道,只有当发送和接收都准备好(其实就是同步),才不会阻塞。

package main

func main() {
	// 示例1。
	ch1 := make(chan int, 1)
	ch1 <- 1
	//ch1 <- 2 // 通道已满,因此这里会造成阻塞。

	// 示例2。
	ch2 := make(chan int, 1)
	//elem, ok := <-ch2 // 通道已空,因此这里会造成阻塞。
	//_, _ = elem, ok
	ch2 <- 1

	// 示例3。
	var ch3 chan int
	//ch3 <- 1 // 通道的值为nil,因此这里会造成永久的阻塞!
	//<-ch3 // 通道的值为nil,因此这里会造成永久的阻塞!
	_ = ch3
}

通道的接收与发送什么时候会引发panic

一句话总结:跟通道channel关闭有关

  • 通道已经关闭,但是依然向其发送元素
package main

func main()  {
	var ch = make(chan int, 2)
	close(ch)
	ch <- 1
}
  • 再次关闭已经关闭的通道
package main

func main()  {
	var ch = make(chan int, 2)
	close(ch)
	//ch <- 1
	close(ch)
}

一个正常的收发channel示例

package main

import "fmt"

func main() {
	var ch = make(chan int, 2)

	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println("sender channel a val: ", i)
			ch <- i
		}
		fmt.Println("sender finished, will close the channel")
		close(ch)
	}()

	for {
		elem, ok := <- ch
		if !ok {
			fmt.Println("channel has already closed.")
			break
		} else {
			fmt.Println("receive a val from channel, ", elem)
		}
	}
	fmt.Println("End.")
}

select

基础概念

select语句是专门为了通道channel而设计的,所以select的每个case表达式中,都只能包含操作通道的表达式,比如接收表达式和发送表达式

select本质上就是对通道channelI/O操作的监听器。

// 给定几个通道,哪个通道不为空,便执行相应语句

// 准备好几个通道。
intChannels := [3]chan int{
  make(chan int, 1),
  make(chan int, 1),
  make(chan int, 1),
}
// 随机选择一个通道,并向它发送元素值。
index := rand.Intn(3)
fmt.Printf("The index: %d\n", index)
intChannels[index] <- index
// 哪一个通道中有可取的元素值,哪个对应的分支就会被执行。
select {
case <-intChannels[0]:
  fmt.Println("The first candidate case is selected.")
case <-intChannels[1]:
  fmt.Println("The second candidate case is selected.")
case elem := <-intChannels[2]:
  fmt.Printf("The third candidate case is selected, the element is %d.\n", elem)
default:
  fmt.Println("No candidate case is selected!")
}

注意点

  • selectcase分支必须是关于通道的发送表达式或者接收表达式。
  • 如果select有默认的default分支,那么无论case条件中对通道的操作是否阻塞,select语句是不会阻塞的。
  • select语句中只能有一个default分支,且分支的位置不重要。都是evaluated各个case分支之后且不满足的时候才会执行default分支的语句。
  • 如果select没有默认的default分支,而且case条件中对通道的操作都阻塞,select语句会阻塞的。如果发生阻塞,就认为该case表达式不满足执行条件,进行下一个case分支的判断。
  • 一个select语句只能对其中的每个case表达式各求值一次,如果想要迭代求值,需要for语句的帮助。但是请注意,如果此时select中的case语句当中有break执行,那么并不会跳出外层的for语句,而是会继续循环。
  • 如果select语句发现同时有多个case分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个执行。注意,即使select语句是在被唤醒时发现的这种的情况,也会这么做。
func example2() {
	intChan := make(chan int, 1)
	// 一秒后关闭通道。
	time.AfterFunc(time.Second, func() {
		close(intChan)
	})
	select {
	case _, ok := <-intChan:
		if !ok {
			fmt.Println("The candidate case is closed.")
			break
		}
		fmt.Println("The candidate case is selected.")
	}
}
// result
The candidate case is closed.

程序分析,代码运行到select的唯一case分支时,因为此时intChan为空,无法进行接收,所以阻塞,1s之后,通道关闭,二元接收值中的okfalse,所以执行答应The candidate case is closed.

再次看一个示例,到底哪个case被执行,或者default被执行

package main

import "fmt"

var channels = [3]chan int{
	nil,
	make(chan int),
	nil,
}

var numbers = []int{0, 1, 2}

func main() {
	select {
	case getChan(0) <- getNumber(0):
		fmt.Println("The first candidate case is selected.")
	case getChan(1) <- getNumber(1):
		fmt.Println("The second candidate case is selected.")
	case getChan(2) <- getNumber(2):
		fmt.Println("The third candidate case is selected")
	default:
		fmt.Println("No candidate case is selected!")
	}
}

func getNumber(i int) int {
	fmt.Printf("numbers[%d]\n", i)
	return numbers[i]
}

func getChan(i int) chan int {
	fmt.Printf("channels[%d]\n", i)
	return channels[i]
}

// result
No candidate case is selected!

为何? 因为三个case分支全都阻塞 第一、第三:试图向nil通道发送数据,阻塞之 第二个试图向无缓冲通道发送,因为没有相应的接收操作,阻塞之 所以只能执行default分支的语句

如果想要第二个case分支运行,只需要做如下修改

package main

import "fmt"

var channels = [3]chan int{
	nil,
	make(chan int),
	nil,
}

var numbers = []int{0, 1, 2}

func main() {
	go func() {
		for {
			<- getChan(1)
		}
	}()
	select {
	case getChan(0) <- getNumber(0):
		fmt.Println("The first candidate case is selected.")
	case getChan(1) <- getNumber(1):
		fmt.Println("The second candidate case is selected.")
	case getChan(2) <- getNumber(2):
		fmt.Println("The third candidate case is selected")
	default:
		fmt.Println("No candidate case is selected!")
	}
	fmt.Println("finish")
}

func getNumber(i int) int {
	fmt.Printf("numbers[%d]\n", i)
	return numbers[i]
}

func getChan(i int) chan int {
	fmt.Printf("channels[%d]\n", i)
	return channels[i]
}

起一个goroutine,接收无缓冲通道的元素

参考资料

1、极客时间《Go语言核心36讲》