持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
🎐 放在前面说的话
大家好,我是北 👧🏻
本科在读,此为日常捣鼓.
如有不对,请多指教,也欢迎大家来跟我讨论鸭 👏👏👏
今天是我们「Go并发」系列的第二篇:「goroutine间的通信桥梁channel」;
Let’s get it
一、channel概念
- 单纯将函数并发执行意义不大,函数与函数间能交换数据才是并发执行函数的意义。
channel是Go中的一个核心类型,我们可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯,即goroutine之间进行通信的重要桥梁channel像传送带医院传输数据,遵循“先进先出”规则- 通过通信来实现共享内存和线程同步,而不是通过共享内存来实现线程间的通信。
- 共享内存在不同的goroutine中容易发送竞态问题,为了保证数据交换的正确性,必须对内存进行加锁,这种做法可能会造成性能问题。
- 同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。
二、创建channel
1. 创建非缓冲channel:
// 使用内建函数make创建channel
ch := make(chan int)
// 使用var声明channel
var ch chan int
- channel 必须定义其传递的数据类型
- 以上两种是双向通道,可以发送数据,也可以接收数据
非缓冲通道:程序中必须同时有不同的goroutine对非缓冲通道进行发送和接收操作,否则会造成阻塞
缓冲通道:可以在同一个goroutine 内接收容量范围内的数据,即使没有另外一个goroutine进行读取
2. 创建缓冲channel
通过缓存的使用,可以尽量避免阻塞,提供应用的性能。
// 创建缓冲大小为33的通道
chstr := make(chan string,33)
三、操作channel
通道具备发送、接收、和关闭三种操作。
1. 发送&接收
默认情况下,channel接收和发送的数据都是阻塞的除非两端俱全,促使goroutine同步简单化,不需要显示的lock。
channel <- value // 发送value到channel
<- channel // 接收并将之丢弃
x := <- channel // 从channel中接收数据,并赋值给x
x, ok := <- channel // 从channel中接收数据,并赋值给x,外加判断通道是否关闭或空
2. 通道关闭
channel支持内置close函数来关闭通道。关闭通道是非必要的,但是手动关闭通道是一个好习惯,并且利用关闭的动作,可以给接收方传递信号(关闭通道一般在发送端关闭)。
被关闭后的通道的特点:
- 被关闭后,再发送数据,将会出现panic
- 关闭后,接收数据,不会影响原有数据的接收,会一直获取数据直到通道为空
- 重复关闭,会panic
- 对已关闭且无数据的通道进行接收操作,得到的对应数据类型的零值
四、实践一下下
1. 综上 for ...range 循环取值
close()函数用于关闭channel,关闭后channel中若还有缓冲数据,仍然可以读取,但是无法再发送数据给已经关闭的channel
// 只写不读
func write(out chan<- int) {
for i := 0; i < 10; i++ {
out <- i
}
close(out)
}
// 只读不写
func read(in <-chan int) {
for num := range in {
fmt.Println(num)
}
}
func main() {
// 创建双向通道
ch := make(chan int)
// 新建协程,生成数据,写入channel
go write(ch)
// 从channel读取内容,打印
read(ch)
}
打印(换行): 0 1 2 3 4 5 6 7 8 9
- 如果把
close(c)注释掉,程序会一直阻塞在for …… range那一行。 - 如果channel c已经被关闭,继续往它发送数据会导致
panic: send on closed channel:
2.判断channel关闭以及单项通道使用
在前面基础上,把main函数里双向通道改为单向通道,并判断通道是否关闭
单向通道,在通道未关闭期间,可以对原本要接收到的通道里的值进行人工干预,比如下面的栗子,我们对原本要接收到的数据,再乘以了它本身。
func main() {
// 创建双向通道
//ch := make(chan int)
//创建单项通道
ch1 := make(chan int)
ch2 := make(chan int)
// 新建协程,生成数据,写入channel
go write(ch1)
// 开启goroutine从ch1中接收数据,并将该值的平方发送到ch2中
go func() {
for {
i, ok := <-ch1 // 通道关闭后再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 从channel读取内容,打印
read(ch2)
}
打印(换行): 0 1 4 9 16 25 36 49 64 81
3. select多路复用
在某些场景下,我们需要同时从多个通道接收数据,如果没有数据,接收可能发送阻塞。然后我们就会运用一下这种方法实现:
for {
data, ok := <-ch1
data, ok := <-ch2
}
可以从多个通道接收到数据,但是性能很差,故此时我们应该用内置select关键字,做同时响应多个通道的操作。
select语句选择一组可能的send操作和receive操作去处理。它类似switch,支持default,如果select没有一条语句可以执行,即所有的通道都被阻塞。
1)格式
select {
case <-c1: //监听channel的读事件
// Do something
case data <- c2: //读事件
// Do something
case c3 <- data: //监控channel的写事件
// Do something
default:
// Do something
}
2)select优势
- 提高代码可读性
- 处理一个或多个channel的发送/接收操作
- 没有case的select{}会一直等待,可用于阻塞main函数
- 有default:select语句不会被阻塞,执行default后,程序的执行会从select语句中恢复,进入下一次轮询。比较消耗资源。
- 没有default:select语句将被阻塞,直到至少有一个通信可以进行下去
- select语句的多个case同时满足条件, 执行哪个case是随机的
- 可以为select设置一个超时时间,当select超时时,可以完成一些其他工作。
3)满足多条件下的select{}
func main() {
ch := make(chan string)
c := make(chan int)
// 向管道ch中放数据
go func() {
time.Sleep(time.Second * 1)
fmt.Println("get into string work")
ch <- "string"
}()
// 向管道c中放数据
go func() {
time.Sleep(time.Second * 1)
fmt.Println("get into int work")
c <- 3
}()
//使用select从管道ch, c中读数据
select {
case n := <-ch:
fmt.Println("get value from ch...", n)
case n := <-c:
fmt.Println("get value from c...", n)
}
time.Sleep(time.Second * 2)
}
4)select{}检测channel的关闭时间
func main() {
start := time.Now()
c := make(chan int)
go func() {
time.Sleep(time.Second * 1)
//c <- 1
close(c)
}()
fmt.Println("Blocking on read...")
select {
case <-c:
fmt.Printf("Unblocked %v later.\n", time.Since(start))
}
}
打印:
Blocking on read...
Unblocked 1.0142148s later.
5)select{}处理超时
在channel准备的时间中,我们想利用这一个时间时,我们可以设置一个超时时间,等待channel准备过程中进行一些处理,栗子如下
func main() {
var c <-chan int
for {
select {
case <-c:
case <-time.After(time.Second * 1):
fmt.Println("Timed out Do something.")
}
}
}
打印:(每隔一秒打印一次)
Timed out Do something.
Timed out Do something.
...
🎉 放在后面的话
以上总结了一些channel 、select 、for...range等相互结合的用法,希望对大家有用。还有很多用法,等以后用到,再慢慢补充。