Go语言通道

背景

Golang与其他语言最大的区别是什么呢?在我看来,一个是goroutine,另一个就是通道channel了。

其他语言,一般通过共享内存的方式实现不同线程间的通信,也就是说,把数据放在共享内存以供多个线程来使用。这种方法思路简单,但却使得并发控制变得复杂和低效。Golang不建议使用这种方式(虽然也提供了这种传统方式),而是推荐使用通道,也就是channel。

详解

基础操作

声明通道

声明一个通道类型如下:

var intChan chan int

初始化通道

通道是一种引用类型,和切片、字典相同。初始化引用类型,都需要用到make关键字,下面代码分别创建了一个非缓冲通道和缓冲通道。

make(chan int)
make(chan int, 5)

发送值到通道

向通道发送一个值。

intChan <-3

从通道接收值

从通道取出一个值。第二种方式更安全,当通道被关闭时,num仍然为0,无法通过num来区分是通道出来的值为0,还是无效值为0,这时就可以通过bool型的ok值来判断是否有效了。

num := <-intChan
num, ok := <-intChan

关闭通道

使用close函数可关闭通道,建议从发送方关闭通道。
关闭通道后,如果尝试向通道发送数据,则会引起运行时恐慌;如果尝试从通道接收值,无论通道是否还有值都会立即返回,可以通过第二个值判断返回值是否有效。

func testM() {
	intChan := make(chan int, 1)

	intChan <- 3
	close(intChan)
	num, ok := <-intChan
	fmt.Printf("get num: %d, ok: %t\n", num, ok)
	num, ok = <-intChan
	fmt.Printf("get num: %d, ok: %t\n", num, ok)
	time.Sleep(time.Second)
}

通道类型

通过make初始化通道时,如果指定了第二个参数通道容量为非零时,那么该通道就是缓冲通道,否则就是非缓冲通道。

非缓冲通道

非缓冲通道,顾名思义,通道无法缓冲任何值,也就是说操作都是同步的。发送方和接收方必须都准备好,否则先执行的发送或者接受goroutine就会被阻塞。

func test1() {
	intChan := make(chan int)
	go func() {
		num := <-intChan
		fmt.Printf("get num: %d\n", num)
	}()

	intChan <- 3
	fmt.Printf("send num: %d\n", 3)

	time.Sleep(2 * time.Second)
}

输出:

get num: 3
send num: 3

上面的例子中,主goroutine发送数据到通道,另外一个goroutine从通道接收数据。我们再加上一点等待时间,看是否会阻塞。

func test2() {
	fmt.Printf("start\n")
	intChan := make(chan int)
	go func() {
		num := <-intChan
		fmt.Printf("get num: %d\n", num)
	}()

	time.Sleep(2 * time.Second)
	intChan <- 3
	fmt.Printf("send num: %d\n", 3)
	time.Sleep(2 * time.Second)
}

我们在发送数据到通道之前,休眠了2秒。程序开始执行后,等待了2秒之后,才有get num的打印,说明这是才收到数据,也就证明了的确会被阻塞。当然,在接收通道之前休眠,也有类似的效果,发送处会被阻塞。
无法缓冲,那么下面的程序会输出什么结果呢?

 func test3() {
	intChan := make(chan int)

	intChan <- 3
	num := <-intChan
	fmt.Printf("get num: %d\n", num)
	time.Sleep(5 * time.Second)
}

换成缓冲通道后,结果又如何呢?

缓冲通道

缓冲通道,也就是说通道可以缓存一定的数据,那么也就可以异步操作了。可以把缓冲通道理解为FIFO,先进先出。

func test2() {
	fmt.Printf("start\n")
	intChan := make(chan int, 5)
	go func() {
		for i := 0; i < 6; i++ {
			num, ok:= <-intChan
			fmt.Printf("get num: %d, %t\n", num, ok)
		}
	}()

	for i := 0; i < 6; i++ {
		intChan <- i
	}
	time.Sleep(2 * time.Second)
}

输出:

start
get num: 0, true
get num: 1, true
get num: 2, true
get num: 3, true
get num: 4, true

可以通过len函数查看通道当前的长度,cap函数查看通道的容量。

func test2() {
	fmt.Printf("start\n")
	intChan := make(chan int, 5)

	for i := 0; i < 5; i++ {
		intChan <- i
		fmt.Printf("len: %d, cap: %d\n", len(intChan), cap(intChan))
	}

	time.Sleep(5 * time.Second)
	fmt.Printf("end\n")
}

输出:

start
len: 1, cap: 5
len: 2, cap: 5
len: 3, cap: 5
len: 4, cap: 5
len: 5, cap: 5
end

处理通道数据

for语句

使用for循环和range来接收通道数据,即使通道被关闭,通道内的数据也能正确的被接收处理,待通道内数据都被接收完后,for语句就会立即结束。可以省掉判断接收值是否有效的代码,更加简洁。

func test1() {
	intChan := make(chan int, 10)
	go func() {
		for i := range intChan {
			fmt.Printf("num: %d\n", i)
		}
		fmt.Printf("receiving ends\n")
	}()

	for i := 0; i < 5; i++ {
		intChan <-i
	}

	close(intChan)

	time.Sleep(10 * time.Second)
}

select语句

select语句可以用来接收和发送通道,其语法形式和switch语句类似,只是select关键字后直接跟大括号。

select语句tip1

select选择不阻塞的case分支时,具有随机性。

func test1() {
	times := 10
	intChan := make(chan int, times)
	for i := 0; i < times; i++ {
		select {
		case intChan <-0:
		case intChan <-1:
		case intChan <-2:
		}
	}
	for i := 0; i < times; i++ {
		fmt.Printf("num: %d\n", <-intChan)
	}

	time.Sleep(1 * time.Second)
}

输出如下。

num: 1
num: 2
num: 1
num: 2
num: 0
num: 0
num: 0
num: 1
num: 1
num: 1
select语句tip2

无论选择哪条case语句,所有case语句里,通道符两边的表达式都会被求值。且执行顺序是从上到下,从左到右。

var intChan1 chan int
var intChan2 chan int
var channels = []chan int{intChan1, intChan2}
var nums = []int{1, 2}

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

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

func test2() {
	fmt.Printf("start\n")
	// channels[0] = make(chan int, 1)
	// channels[1] = make(chan int, 1)
	select {
	case getChan(0) <-getNum(0):
		fmt.Printf("case 0\n")
	case getChan(1) <-getNum(1):
		fmt.Printf("case 1\n")
	default:
		fmt.Println("default case")
	}

	time.Sleep(1 * time.Second)
	fmt.Printf("end\n")
}

输出:

start
channel[0]
nums[0]
channel[1]
nums[1]
default case
end

两个通道都未被初始化,发送数据到通道会被阻塞,所以最终选择了执行default分支。但前面case两边的表达式都被求值了。
再分别取消示例中对通道初始化的注释,再执行,可以看到无论选择哪条分支,case两边的表达式都会被求值。

select语句tip3

如果所有分支都阻塞,且没有default分支,那么该select语句就会被阻塞。

func test3() {
	fmt.Printf("start\n")
	// A deadlock will happen
	select {
	case getChan(0) <-getNum(0):
		fmt.Printf("case 0\n")
	case getChan(1) <-getNum(1):
		fmt.Printf("case 1\n")
	}

	time.Sleep(1 * time.Second)
	fmt.Printf("end\n")
}

输出:

start
channel[0]
nums[0]
channel[1]
nums[1]
fatal error: all goroutines are asleep - deadlock!

由于是主goroutine,且没有其他goroutine,就发生了死锁。

参考

https://www.cnblogs.com/wt645631686/p/9657711.html
《Go并发编程实践》

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值