go-Channel

1 概述

通道是Golang提供的一种基本类型,它可以实现在协程之间的单向通信和双向通信、发送和接收数据、以及协程同步。

channel的本质是一个队列,遵循先进先出原则。channel是线程安全的,在任何给定时间,一个数据被设计为只有一个协程可以对其访问,不会发生数据竞争,多goroutine访问时,也不需要加锁,这些是有编译器在底层维护的。

2 分类

在Go中,通道有两种主要类型:

非缓冲通道:发送操作会一直阻塞,直到有一个goroutine接收到了这个消息;

带缓冲通道:发送操作不会被阻塞,直到通道内的消息数量超过了缓冲区的大小;

3 声明与初始化

3.1 声明

var chanName chan toTransDatatype
var funcChan chan func(int) error
var chanChan chan chan int

通道的数据类型为 chan,后面的 toTransDataType 表示 想要传递的数据类型,如 int、float、string、interface{}、结构体、指针、func()等,甚至 chan本身。

var m map[string] chan bool

上面的代码声明了一个 m 的 map,它的key为string类型,它的value 为 chan bool 类型

3.2 初始化

channelWithoutBuf := make(chan int) // 无缓冲通道
channelWithBuf := make(chan int, 3) // 有缓冲通道,缓冲区size为3,提前分配了内存

 3.3 示例

package main

import "fmt"

func main() {
	var a chan int
	fmt.Println(fmt.Sprintf("a :%+v, type: %T", a, a))
	b := make(chan int)
	fmt.Println(fmt.Sprintf("b :%+v, type: %T", b, b))
	c := make(chan int, 3)
	fmt.Println(fmt.Sprintf("c :%+v, type: %T", c, c))

	a = b
	fmt.Println(fmt.Sprintf("new a :%+v, type: %T", a, a))

	b = c
	fmt.Println(fmt.Sprintf("new b :%+v, type: %T", b, b))
	return
}

输出

a :<nil>, type: chan int
b :0x14000016540, type: chan int
c :0x14000100000, type: chan int
new a :0x14000016540, type: chan int
new b :0x14000100000, type: chan int

可以看到,通过 var 声明的通道,值为nil,相当于是一个空管道。

把 b 赋值给 a,此时 a 也就成了 b;同时,把 b 赋值给 c,b也会变成 c,说明初始化的不同类型通道之间是可以互相转化的。

4 操作

通道的操作符为 <-,语法是:

  • chanName<- value:把value变量的值写入chanName通道;
  • value := <-chanName:从chanName中取出数据,赋值给value变量;

 我们来试试:

package main

import "fmt"

func main() {
	var chanA chan int
	chanA <- 1
	data := <-chanA
	fmt.Println(fmt.Sprintf("data: %+v, type:%T", data, data))
	//close(chanA)
	//// 由管道中读写数据,<-操作符是与最左边的chan优先结合的
	// 向管道中写入一个数据,在此需要注意:向管道中写入数据通常会导致当前协程阻塞,直到有其他协程从这个管道中读取数据
}

输出

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send (nil chan)]:
main.main()
        /Users/yiyunjie/go/src/Test/main.go:7 +0x28

Process finished with the exit code 2

可以看到,main协程非正常退出,所有的协程处于休眠状态 - 死锁!所谓死锁,就是双方都在等待对方释放锁。

在当前场景,就是两个协程一直在等待对方释放资源。而在这个例子中,只有一个main函数所在的协程,并没有新的协程参与,为什么会死锁呢?

原因在于 往一个无缓冲通道里写入数据时,发送操作会一直阻塞、等待返回,直到有一个协程接收到了这个通道里的数据,阻塞状态才会解除。而上面的例子,是直接在当前协程接收数据,而当前协程一直在阻塞,不能继续往下执行,所以接收数据的代码根本就没执行到。这样程序就一直在等待一个无法释放的锁,形成了死锁。Go的运行时系统检测到这种情况后,会抛出这个错误,并终止程序的执行。

要解决这个问题,有两种方法:将发送操作变成非阻塞的;保持发送操作为阻塞的,用另一个协程接收数据;

4.1 非阻塞发送

我们知道,往带缓冲通道里写入数据是非阻塞的,直到通道内的消息数量超过了缓冲区的大小。那我们就定义一个带缓冲通道试试:

package main

import (
	"fmt"
)

func main() {
	chanA := make(chan int, 3)
	chanA <- 1
	data := <-chanA
	fmt.Println(fmt.Sprintf("data: %+v, type: %T", data, data))
	return
}

输出

data: 1, type: int

没有问题,可以正常接收到数据,且类型相符。

4.2 用新协程接收非缓冲通道信息

由于往非缓冲通道发送数据是阻塞的,所以不能把接收数据的新协程写在发送操作之后,不然当前协程一直阻塞,新协程还是无法接受到数据。所以只能把接收动作放在发送之前。

package main

import (
	"fmt"
	"time"
)

func main() {
	chanA := make(chan int)
	go func() {
		data := <-chanA
		fmt.Println(fmt.Sprintf("data: %+v, type: %T", data, data))
	}()
	chanA <- 1
	time.Sleep(10 * time.Millisecond)
	return
}

输出

data: 1, type: int

5 非缓冲通道的阻塞(待续)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章