GolangCSP模型中,channel在通信和同步中有着重要的作用。本文主要从基本概念、常见的用法、遇到的问题三个方面总结channel的用法。

Golang Channel 是什么

channel在中文交流中一般叫做通道

在CSP(Communicating Sequential Processes)并发模型中,核心的概念是两个并发实体通过channel进行通信,Golang中借用其中的一些概念,比如processchannel等。利用channel可以让goroutine之间通过通信来共享内存,我们可以将channel看做内部的FIFO队列。一些goroutine将数据发送到队列,而其他goroutine从队列中接收数据。

Golang Channel 的使用

Golang Channel 中的数据类型

在Go语言中,使用chan关键字创建一个通道,并且该通道只能传输相同类型的数据,不允许从同一通道传输不同类型的数据。

type T struct{}
var noBufferCh1 chan T
var noBufferCh2 = make(chan T)
var bufferCh = make(chan T, 10)

在Go语言中,通道工作有两个主要操作,一个正在发送,另一个正在接收。在通道中,发送和接收操作将阻塞,直到另一侧准备好为止。它允许goroutine在没有显式锁或条件变量的情况下彼此同步。

Golang Channel 的操作

发送操作

发送操作用于在通道的帮助下将数据从一个goroutine发送到另一个goroutine

intfloat64bool之类的值可以安全且容易地通过通道发送,因为它们是被复制的,因此不存在意外并发访问相同值的风险。同样,字符串也是安全的,因为它们是不可变的。但是对于通过通道发送指针引用(例如slice,map等)并!!!不安全!!!,因为指针或引用的值可能会在发送到通道的同时或之后发生修改,甚至发送端goroutine或接收goroutine可能对其更改,并且结果和修改的时间不可预测。因此,在通道中使用指针或引用时,除非你明确知道会发生什么,否则必须确保它们一次只能由一个goroutine访问

我曾经遇到过一次这样的情况,我维护了一个responseCh用来缓冲响应数据,但当responseCh消费比较慢时,某一条responseCh中的消息,被后续的逻辑修改。导致输出到终端的信息与我推到responseCh时不一致。示例代码如下,在一个复杂的系统中,很有可能就会因为疏忽导致这样的错误,需要自我检讨。

func main() {
    type T struct{
        Name string
    }
    ch := make(chan T, 1000)
    go func() {
        for{
            select {
            case msg := <- ch:
                fmt.Println(msg)
            }
        }
    }()
    a := T{Name: "aaa"}
    ch <- a
    a.Name = "bbb"

    select{}
}

接收操作

接收操作用于接收发送操作员发送的数据。

关闭通道

使用close函数可以关闭通道。这是一个内置函数,并设置一个标志,指示不再有任何值将发送到该通道。向一个已关闭的通道发送数据,将会引发一个panic。

创建通道

每个通道值都有一个容量,容量为零的通道值称为无缓冲通道,容量为非零的通道值称为缓冲通道。 channel的零值为nil。 必须使用内置make函数来创建非nil通道值。

如下代码

//双向通道
var A = make(chan int)

//只入通道
var B = make(chan<- int)

//只出通道
var C = make(<-chan int)

//双向带缓冲区的通道
var A = make(chan int, 10)

make函数调用的第二个参数指定新创建的通道的容量。第二个参数是可选的,其默认值为零。

Channel Select 操作

select允许同时等待多个通道操作,将goroutines和通道与select结合在一起,可以实现一些强大功能。select时会阻塞,直到其中一个case可以运行,然后执行那个case。如果有多个,它会随机选择一个,一定注意这里是随机选择一个case执行。类似的语法比如switch,但switch是有顺序的,这里要注意区别。

//
var msgIdCh = make(chan int64, 10)
var msgCh = make(chan string, 10)

select{
case content :=<-msgCh:
    fmt.Println(content)
case id := <- msgCh:
    fmt.Println(id)
}

wg := sync.WaitGroup{}
wg.Add(1)
go func() {
    defer wg.Down()
    for {
        msgIdCh <- time.Now().Unix()
    }
}()

wg.Add(1)
go func() {
    defer wg.Down()
    for {
        msgCh <- time.Now().String()
    }
}()

wg.Wait()

总结

  1. 只使用有效的channel
  2. select-case是随机的
  3. 读写有先后,乱序会阻塞
  4. 函数间直接传递 channel,不需要传 channel 的指针,因为 chan 的变量本身就是一个指针