kevinyan815 / gocookbook

go cook book
MIT License
788 stars 167 forks source link

Channel 基础概念和用法 #54

Open kevinyan815 opened 3 years ago

kevinyan815 commented 3 years ago

Go 语言中最常见的、也是经常被人提及的设计模式就是:不要通过共享内存的方式进行通信,应该通过通信的方式共享内存。在很多主流的编程语言中,多个线程传递数据的方式一般都是共享内存,为了解决线程竞争,我们需要通过加互斥锁的方式限制同一时间能够读写这些变量的线程数量。

Thread1 ====> Memory ====> Thread2

然而这与 Go 语言鼓励的设计并不相同。

虽然我们在 Go 语言中也能使用共享内存加互斥锁进行通信,但是 Go 语言提供了一种不同的并发模型,即通信顺序进程(Communicating sequential processes,CSP)。goroutine 和 Channel 分别对应 CSP 中的实体和传递信息的媒介,goroutine 之间会通过 Channel 传递数据。

goroutine1 ====> Channel ====> goroutine2

上面两个 goroutine,一个会向 Channel 中发送数据,另一个会从 Channel 中接收数据,它们两者能够独立运行并不存在直接关联,但是能通过 Channel 间接完成通信。

目前的 Channel 收发操作均遵循了先进先出的设计,具体规则如下:

基本用法

可以往 Channel 中发送数据,也可以从 Channel 中接收数据,所以,Channel 类型分为:只接收、只发送、双向收发 三种类型。

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

相应的语法定义如下:

类型声明

var chan1 chan int // 可以双向收发int数据的 双向chan

var chan2 <-chan int // 只能从通道接收到int数据的 单向chan

var chan3 chan <- int //只能向 chan 发送int 数据的 单向chan

初始化

未初始化的 chan 的零值是 nil,不能直接使用。通过内置的 make 函数,我们可以初始化一个 chan

make(chan int, 100)

上面我们初始化了一个容量为100的 chan ,我们把这样的 chan 叫做 buffered chan。如果没有设置容量,那么容量默认是 0,我们把这样的 chan 叫做 unbuffered chan。

make(chan int)

对于 buffered chan,如果 chan 的缓存循环列表中还有数据,那么goroutine 从 chan 接收数据的时候就不会阻塞,如果 chan 的容量还未填满,goroutine 给它发送数据时也不会阻塞,否则就会发送阻塞。unbuffered chan 只有读写都准备好之后才不会阻塞

发送数据

往通道中发送一个数据使用 ch<-,发送数据是一条语句:

ch <- 2000

这里的 ch 是 chan int 类型或者是 chan <-int 类型。

接收数据

从通道中接收一条数据使用 <-ch,接收数据也是一条语句:

x := <-ch // 把接收的一条数据赋值给变量x
foo(<-ch) // 把接收的一个的数据作为参数传给函数
<-ch // 丢弃接收的一条数据

这里的 ch 类型是 chan T 或者 <-chan T。

从通道中接收数据时,还可以返回两个值。

v, ok := <-ch

第一个值 v 存储从 chan 中读取到的元素,第二个值是 bool 类型,代表是否成功地从 chan 中读取到一个值,如果第二个参数是 false,表明 chan 已经被 close 而且 chan 中没有缓存的数据,这个时候,第一个值是零值。所以,如果从 chan 读取到一个零值,可能是 sender 真正发送的零值,也可能是 chan 已经被 close 并且已经没有缓存元素而产生的零值。

其他操作

Go 内建的函数 close、cap、len 都可以操作 chan 类型:close 会把 chan 关闭掉,cap 返回 chan 的容量,len 返回 chan 中缓存的还未被取走的元素数量。

send 和 recv 都可以作为 select 语句的 case clause,如下面的例子:


func main() {
    var ch = make(chan int, 10)
    for i := 0; i < 10; i++ {
        select {
        case ch <- i:
        case v := <-ch:
            fmt.Println(v)
        }
    }
}

chan 还可以应用于 for-range 语句中,比如:


    for v := range ch {
        fmt.Println(v)
    }

在通道被关闭后,会自动退出for range 循环的执行。

下面的可以用于抽干Channel内缓存的元素

for range ch {
}

// 或者 
for _ := range ch {

}

Channel和并发同步原语怎么选择

Channel 并不能用于解决所有并发编程中的问题,有些场景适合使用Channel,而有的场景使用并发同步原语更加简单。 具体什么时候用Channel 什么时候用 sync 原语,可以用一下标准衡量: