Golang——channel
Go语言中的channel是一种用于协程之间通信的机制。在并发编程中,协程之间的通信是非常重要的,因为它可以使得不同的协程之间协同工作,从而实现更高效的程序执行。本文将详细介绍Go语言中的channel,包括其用法、特性、以及一些实践等。
一、不同的Goroutine之间如何通讯
1.1 全局变量加锁同步
package main
import (
"fmt"
"sync"
"time"
)
var (
// make(Type, len, cap)
// len:数据类型实际占用的内存空间长度,map、 channel 是可选参数,slice 是必要参数
myMap = make(map[int]int, 10)
// 声明一个全局的互斥锁
lock sync.Mutex
)
// 编写一个函数来计算各个数的阶乘并放入到map中
// 启动多个协程,map应该做成全局的
func getGactorial(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
// 加锁访问map
lock.Lock()
myMap[n] = res
// 解锁
lock.Unlock()
}
func main() {
// 开启多个协程完成这个任务
for i := 1; i <= 10; i++ {
go getGactorial(i)
}
// 休眠5秒等协程都运行完
time.Sleep(time.Second * 5)
// 输出map中的计算结果
lock.Lock()
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
lock.Unlock()
}
1.2 使用channel进行同步(更具有go特色的做法)
- channel本质类似于一个队列
- 数据是先进先出
- 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
- channel是有类型的,一个string的channel只能存放string类型的数据
- channel是引用类型
- channel必须初始化才能写入数据,即make后才能使用
- 向管道中写入数据时不能超过其容量,容量在make的时候会指定
- 在没有使用协程的情况下,如果管道数据已经全部取出,再取就会报错deadlock
- 在默认情况下,管道是双向的,但也可以声明为只读或只写
// 声明为只写
var chan1 chan<- int
chan1 = make(chan int, 3)
chan1 <- 20
// num := <- chan1 //Error
fmt.Println("chan1 = ", chan1)
// 声明为只读
var chan2 <-chan int
num := <-chan2
fmt.Println("num = ", num)
二、带缓存的channel
2.1 channel的声明:
var 变量名 chan 数据类型
// 管道的使用
package main
import "fmt"
func main() {
// 创建一个可以存放3个int类型的管道
var intChan chan int // 声明管道
intChan = make(chan int, 3) // 初始化管道(必须),cap是3
// 向管道写入数据,最多写入三个数据
intChan <- 10
num := 216
intChan <- num
intChan <- 50
// 查看管道的长度和容量(cap)
fmt.Printf("channel len = %d, cap = %d\n", len(intChan), cap(intChan)) // 3, 3
// 从管道中取数据
// 先进先出,取出一个数据则len变小,但容量cap不变
var num1 int
num1 = <-intChan
fmt.Println("num1 = ", num1)
fmt.Printf("channel len = %d, cap = %d\n", len(intChan), cap(intChan)) // 2, 3
num2 := <-intChan
num3 := <-intChan
num4 := <-intChan // 这个会报错
fmt.Println("num2 = ", num2, "num3 = ", num3, "num4 = ", num4) // deadlock
}
存放任意数据类型的管道(空接口实现,注意类型断言的使用)
// 管道的使用
package main
import "fmt"
type Cat struct {
Name string
Age int
}
func main() {
// 创建一个可以存放3个任意数据类型的管道(空接口来实现)
allChan := make(chan interface{}, 3)
allChan <- 10
allChan <- "tom"
cat := Cat{"mimi", 4}
allChan <- cat
// 如果我们希望获得管道中的第三个元素,则先将前2个推出
<-allChan
<-allChan
// 从管道中取出猫猫
newCat := <-allChan
fmt.Printf("newCat=%T, newCat=%v\n", newCat, newCat) // newcat=main.Cat, newcat={mimi 4}
// fmt.Printf("newCat.Name=%v", newCat.Name) 编译不通过,因为newcat实际是空接口类型,不能访问属性,需要类型断言!!!
a := newCat.(Cat)
fmt.Printf("newCat.Name=%v", a.Name)
}
2.2 channel的关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据,但是仍然可以从该channel读取数据
2.3 channel的遍历
channel支持for-range的方式进行遍历
- 在遍历时,如果channel没有关闭,则会出现deadlock错误
- 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
intChan := make(chan int, 3)
intChan <- 1
intChan <- 2
intChan <- 3
close(intChan) // 关闭channel,之后不能再往其中写,但可以读取
// 遍历管道用for-range
for v := range intChan {
fmt.Println("v = ", v)
}
三、不带缓存的channel
一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。
ch = make(chan int) // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。
四、串联的channels(pipeline)
Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)
例子:
第一个goroutine是一个计数器,用于生成0、1、2、……形式的整数序列,然后通过channel将该整数序列发送给第二个goroutine;第二个goroutine是一个求平方的程序,对收到的每个整数求平方,然后将平方后的结果通过第二个channel发送给第三个goroutine;第三个goroutine是一个打印程序,打印收到的每个整数。
当一个channel被关闭后,再向该channel发送数据将导致panic异常。当一个被关闭的channel中已经发送的数据都被成功接收后,后续的接收操作将不再阻塞,它们会立即返回一个零值。
没有办法直接测试一个channel是否被关闭,但是接收操作有一个变体形式:它多接收一个结果,多接收的第二个结果是一个布尔值ok,ture表示成功从channels接收到值,false表示channels已经被关闭并且里面没有值可接收。
// Squarer
go func() {
for {
x, ok := <-naturals
if !ok {
break // channel was closed and drained
}
squares <- x * x
}
close(squares)
}()
但是上面这样写很笨,推荐使用for-range循环,它依次从channel接收数据,当channel被关闭并且没有值可接收时跳出循环。
func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; x < 100; x++ {
naturals <- x
}
close(naturals)
}()
// Squarer
go func() {
for x := range naturals {
squares <- x * x
}
close(squares)
}()
// Printer (in main goroutine)
for x := range squares {
fmt.Println(x)
}
}
其实并不需要关闭每一个channel。**只有当需要告诉接收者goroutine,所有的数据已经全部发送时才需要关闭channel。**不管一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收。(不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件,都需要在不使用的时候调用对应的Close方法来关闭文件。)
五、使用select解决从管道取数据的阻塞问题
传统方法在遍历管道时,如果不关闭会阻塞而导致deadlock,当不确定什么时候关闭管道时,可以使用select方法解决
package main
import (
"fmt"
"time"
)
func main() {
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
// label:
for {
select {
case v := <-intChan: // 如果intChan一直没有关闭,不会一直阻塞而deadlock,会自动到下一个case匹配
fmt.Printf("从intChan读取的数据%d\n", v)
time.Sleep(time.Second)
case v := <-stringChan:
fmt.Printf("从stringChan读取的数据%s\n", v)
time.Sleep(time.Second)
default:
fmt.Printf("都取不到了,不玩了,程序员可以加入逻辑\n")
time.Sleep(time.Second)
return
// break label
}
}
}
六、goroutine和channel结合使用案例
利用goroutine和channel协同工作实现下面的要求:
- 开启一个
writeData协程,向管道intChan中写入50个整数。 - 开启一个
readData协程,从管道intChan中读取writeData写入的数据。 - 注意:
writeData和readData操作的是同一个管道。 - 主线程需要等待
writeData和readData协程都完成工作才能退出管道。
package main
import "fmt"
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
intChan <- i
fmt.Println("writeData ", i)
}
close(intChan) // 写完了关闭管道并不影响读
}
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok { // intChan读完才会退出
break
}
fmt.Printf("readData读到数据 = %v\n", v)
}
// 读取完数据后,关闭exitChan
close(exitChan)
}
func main() {
// 创建两个管道
intChan := make(chan int, 50)
exitChan := make(chan bool, 1) // 退出管道,用于主线程检测退出协程完成标志
go writeData(intChan)
go readData(intChan, exitChan)
// 检查exitChan中的协程完成标志来确保协程任务完成后主线程才结束
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
-
一个channel有发送和接受两个主要操作,都是通信行为。
-
一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。
-
发送和接收两个操作都使用
<-运算符。 -
在发送语句中,
<-运算符分割channel和要发送的值。 -
在接收语句中,
<-运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的。