前言
Goroutine和Channel是Go语言实现并发编程的两大利器,本文是Channel介绍的第一篇,主要介绍:
- 1.什么是Channel
- 2.Channel的基本使用
- 3.Channel使用的注意事项
- 4.为什么会有单向Channel
- 5.Channel的使用场景
什么是Channel
Channel是Go语言中比较重要的数据结构,你可以将其看做一条管道,可以往里面写入数据,也可以从里面读取数据。
Channel也是Goroutine之间通信的桥梁,并且是线程安全的,通过和Goroutine搭配,实现了比较著名的CSP模型。
Channel遵循先进先出的原则,这依赖其内部的互斥锁,Channel在读取和写入数据前,都会进行加锁,这个会在后面Channel的实现原理中讲到。
Channel的基本使用
// 初始化一条管道,这条管道只能传送 int 类型的数据
ch := make(chan int)
// 初始化一条管道,且带有缓冲区
ch := make(chan int, 3)
// 往channel中写入数据的语法
ch <- v
// 从channel中读取数据的语法:从channel中读取值,并赋给v
v := <-ch
// 遍历channel中的数据
for i := range ch {
fmt.Println(i)
}
Channel的基本语法非常简单,但是非常容易出现死锁,或者panic,这个会在下面的注意事项中进行详细描述。
Channel的注意事项
前面提到,Channel本身的语法并不难,难的是一不小心产生的死锁,以及panic。这是我对不同状态下的Channel进行读取、写入和关闭之后的一些总结:
场景 | 读取 | 写入 | 关闭 |
---|---|---|---|
未初始化的channel | 死锁 | 死锁 | panic |
已初始化的channel(无缓冲) | 看具体代码 | 看具体代码 | 正常 |
已初始化的channel(有缓冲) | 看具体代码 | 看具体代码 | 正常 |
已关闭的channel | 正常 | panic | panic |
注:死锁 和 panic 并不太一样,大家可以自行查询。
1.对未初始化的channel进行读写关
- 1.当channel未初始化,往channel写入数据,会造成死锁(deadlock!)
- 2.当channel未初始化,往channel读取数据,会造成死锁(deadlock!)
- 3.当channel为初始化,关闭该channel,会触发panic
// 测试用例
// 往一个未初始化的channel写数据
func unInitTest1() {
var ch chan int
ch <- 1
}
// 从一个未初始化的channel读数据
func unInitTest2() {
var ch chan int
<- ch
}
// 关闭一个未初始化的channel
func unInitTest3() {
var ch chan int
close(ch)
}
2.对已初始化的channel进行读写关
对已初始化的channel进行读写,有可能导致死锁,需要看具体的代码,
1.当channel已初始化,但没带缓冲
// channel容量/缓冲为0,只有写入没有读取,死锁
// fatal error: all goroutines are asleep - deadlock!
func initTest1() {
c := make(chan int)
c <- 10
}
// channel容量/缓冲为0,只有读取没有写入,死锁
func initTest2() {
c := make(chan int)
data, ok := <-c
if ok {
fmt.Println(data)
}
}
// channel容量/缓冲为0,看似有读取,也有写入,实际第一步的时候已经死锁
func initTest3() {
c := make(chan int)
// 第一步
data, ok := <-c
if ok {
fmt.Println(data)
}
// 第二步
c <- 10
}
// 正常,此时的channel相当于一条管道,将写入和读取两端直接连接起来
// 技巧:对于不带缓冲的channel,读取要比写入前执行,且不能阻塞
func initTest4() {
c := make(chan int)
go func() {
// 第一步
data, ok := <-c
if ok {
fmt.Println(data)
}
}()
// 第二步
c <- 10
}
2.当channel已初始化,但带了缓冲(容量不为0)
// 正常
func initTest5() {
c := make(chan int, 1)
c <- 10
}
// 死锁,因为此时检测到没有"写入的goroutine",所以读取会一直阻塞,所以造成死锁
func initTest6() {
c := make(chan int, 1)
data, ok := <-c
if ok {
fmt.Println(data)
}
}
// 正常,此时的channel相当于一条管道,将写入和读取两端直接连接起来
// 技巧:对于带缓冲的channel,写入要比读取先执行,且不能溢出(阻塞)
func initTest7() {
c := make(chan int, 1)
c <- 10
data, ok := <-c
if ok {
fmt.Println(data)
}
}
3.对已经关闭的channel进行读写
- 1.当channel已经关闭,往channel写入数据,会造成panic (panic: send on closed channel)
- 2.当channel已经关闭,从channel读取数据,不会造成panic。如果channel中还有未读取的消息,仍然能继续读出;如果没有未读的消息,则会读取到该类型的零值(默认值)。
- 3.当channel已经关闭,再次关闭channel(重复关闭channel),会造成panic (panic: close of closed channel)。
单向Channel
双向Channel
指这个管道既可以发送数据,也可以读取数据;单向Channel
指这个管道只能读,或者只能写。
可能有人会疑惑为什么会有单向Channel
,毕竟只读、或者只写channel并没有意义。
我一开始也有这种疑惑,后面发现,单向channel的主要作用是避免channel滥用,并且能在编译前发现我们的逻辑错误。
我们以生产者消费者的例子来说明,对于生产者而言,从逻辑上看,或者从它的职责上看,它只会发送数据;而对于消费者而言,它只会接收数据。对于这种场景,我们就可以定义两个Channel,一个是只读,一个是只写,如果只读的Channel被写入数据,或者只写的Channel被用于读取,就可以提前检测出来!
package main
import (
"fmt"
"sync"
"time"
)
// 生产者与消费者
var wg sync.WaitGroup
func main() {
ch := make(chan int, 100)
go consumer(ch)
producer(ch)
wg.Wait()
fmt.Println("finished!")
}
// 定义一个只写的channel
func producer(sender chan <- int) {
for i := 1; i < 10; i++ {
sender <- i
wg.Add(1)
time.Sleep(1 * time.Second)
}
}
// 定义一个只读的channel
func consumer(reader <-chan int) {
fmt.Println("consumer")
for {
data, ok := <-reader
if !ok {
fmt.Println("no data")
}
fmt.Printf("consumer %d\n", data)
wg.Done()
}
}
上面的例子利用了Channel的两个特性:
- 1.Channel的传参是引用传递的。所以对单向Channel的修改,都会反映到原始的双向Channel上。
- 2.双向Channel是可以自动转换为单向Channel的。所以一般都是在函数外层声明一个双向Channel,然后在函数参数声明的是一个单向Channel。
Channel的一些使用场景
1.搭配Goroutine实现并发编程
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}
这是Go Tour中的一个例子,利用Goroutine和Channel实现数组的分批求和。
2.搭配Goroutine实现异步、解耦
package main
import (
"fmt"
"sync"
"time"
)
// 生产者与消费者
var wg sync.WaitGroup
var ch = make(chan int, 100)
func main() {
go consumer()
producer()
wg.Wait()
fmt.Println("finished!")
}
func producer() {
for i := 1; i < 10; i++ {
ch <- i
wg.Add(1)
time.Sleep(1 * time.Second)
}
}
func consumer() {
fmt.Println("consumer")
for {
data, ok := <-ch
if !ok {
fmt.Println("no data")
}
fmt.Printf("consumer %d\n", data)
wg.Done()
}
}
上面的例子是一个典型的生产者消费者的例子,通过Channel实现生产者和消费者的解耦,这种思想也常用于日志的写入。
3.使用Channel做锁
使用Channel做锁,主要利用了Channel的缓冲区已满,继续写入数据会阻塞;以及缓冲区为空,继续读取数据会阻塞的特性。