golang之Channel

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/phantom_111/article/details/79489313

Channel是Go中的一个核心类型,可以将其看成一个管道,通过它并发单元就可以发送或者接收数据进行通信(communication)。

Do not communicate by sharing memory; instead, share memory by communicating.

channel基础知识

创建channel

使用内建函数make创建channel:

unBufferChan := make(chan int)  // 1 无缓冲的channel
bufferChan := make(chan int, N) // 2 带缓冲的channel
  • 无缓冲: 发送和接收动作是同时发生的。如果goroutine读取channel(<-channel),则发送者(channel<-)会一直阻塞。
  • 缓冲 channel 类似一个有容量的队列。当队列满的时候发送者会阻塞;当队列空的时候接收者会阻塞。

写出以下代码打印的结果:

func main() {
    var x chan int
    go func() {
        x <- 1
    }()
    <-x
}

结果分析:往一个nil channel中发送数据会一直被阻塞,从一个nil channel中接收数据会一直被block,所以才会产生如下死锁的现象。

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive (nil chan)]:
main.main()
    /Users/kltao/code/go/examples/channl/channel1.go:11 +0x60

goroutine 4 [chan send (nil chan)]:
main.main.func1(0x0)fatal error: all goroutines are asleep - deadlock!

结论:channl 使用要小心,使用前切记要初始化,初始化函数用make。

channel读写操作

ch := make(chan int, 10)

// 读操作
x <- ch

// 写操作
ch <- x

channel关闭

channel可以通过内建函数close()来关闭:

ch := make(chan int)

// 关闭
close(ch)

关闭channel注意事项

  • 重复关闭channel会导致panic。
  • 向关闭的channel发送数据会panic。
  • 从关闭的channel读数据不会panic,读取出channel中已有的数据之后再读就是channl类型的默认值,比如chan bool类型的channel关闭之后读取的值为false。

区分channl中读取的默认值还是channl中传输的值,采用ok_idiom方式:

ch := make(chan int, 10)
...
close(ch)

// ok-idiom 
val, ok := <-ch
if ok == false {
    // channel closed
}

channel使用

select

golang的select的功能和select 、poll、epoll类似,就是监听IO操作,当IO操作发生时触发响应的动作(select 的case里的语句只能是IO操作) 。select使用一般配合for循环使用。

示例1
ch1 := make (chan int, 1)
ch2 := make (chan int, 1)

...

select {
case <-ch1:
    fmt.Println("ch1 pop one element")
case <-ch2:
    fmt.Println("ch2 pop one element")
}

结果

此示例里面 select 会一直等待等到某个 case 语句完成, 也就是等到成功从 ch1 或者 ch2 中读到数据。 则 select 语句结束。

示例2—使用select实现timeout机制


import "time"
import "fmt"
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
}

结果

这个例子我们会在2秒后往channel c1中发送一个数据,但是select设置为1秒超时,因此我们会打印出timeout 1,而不是result 1

示例3—带有default的select

““go
ch1 := make (chan int, 1)
ch2 := make (chan int, 1)

select {
case <-ch1:
fmt.Println(“ch1 pop one element”)
case <-ch2:
fmt.Println(“ch2 pop one element”)
default:
fmt.Println(“default”)
}
““

结果

ch1 和 ch2 都为空,所以 case1 和 case2 都不会读取成功。 则 select 执行 default 语句。

作用

因为这个 default 特性, 我们可以使用 select 语句来检测 chan 是否已满。示例代码如下:

ch := make (chan int, 1)
ch <- 1
select {
case ch <- 2:
default:
    fmt.Println("channel is full !")
}
//ch 插入 1 的时候已经满了, 当 ch 要插入 2 的时候,发现 ch 已经满了(case1 阻塞住), 则 select 执行 default 语句。 这样就可以实现对 channel 是否已满的检测, 而不是一直等待。

range channel

range channel 可以直接取到 channel 中的值。当我们使用 range 来操作 channel 的时候,一旦 channel 关闭,channel 内部数据读完之后循环自动结束。

func consumer(ch chan int) {
    for x := range ch {
        fmt.Println(x)
        ...
    }
}

func producer(ch chan int) {
  for _, v := range values {
      ch <- v
  }  
}

单向channel

单向channel,顾名思义只能写或读的channel。但是在实际使用中用处不大,单向channel主要用于函数声明。比如:

func foo(ch chan<- int) <-chan int {...}
//foo 的形参是一个只能写的 channel,那么就表示函数 foo 只会对 ch 进行写,当然你传入的参数可以是个普通 channel。foo 的返回值是一个只能读的 channel,那么表示 foo 的返回值规范用法就是只能读取。

单向channel在功能上和普通的channel并没有太大区别。但是使用单向channel编程体现一种非常优秀的编程范式:convention over configuration(约定由于配置)。

channel的典型用法

同步goroutine

chan是go里的第一对象,所以可以把chan传入chan中。具体示例如下:

““go
func main() {
g := make(chan int)
quit := make(chan chan bool)
go Channel(g, quit)
for i := 0; i < 5; i++ {
g<-i
}
wait := make(chan bool)
quit <-wait
<-wait //等Channel函数中传入的bool类型的chan有值写入的时候才会继续执行
fmt.Println(“Main Quit”)
}

func Channel(g chan int, quit chan chan bool) {
for {
select {
case i := <-g:
fmt.Println(i)
case c := <-quit: //接收main函数传递的bool类型的channel
c <- true //向其中写入值true
fmt.Println(“channel function Quit”)
return
}
}
}

/* output*/
0
1
2
3
4
B Quit
Main Quit
““

基于channel、gorutine实现工作池

worker工作池负责处理任务jobs,然后将加工后的结果写入到result,所以此处需要两个通道。jobs负责任务的传递,results负责结果的传输。这里启动3个worker,初始是阻塞的,因为还没有任务传递。然后传递9个任务到jobs中并关闭通道表示全部任务发送完毕。最后调用sumResult()统计任务返回的结果。

func worker(id int, jobs chan int, results chan int) {
    for j := range jobs {
        fmt.Println("worker", id, "process job", j)
        time.Sleep(time.Second)
        results <- j * 2 
    }   
}

func sumResult(results chan int) {
    var sum int 
    for a := 1; a <= 9; a++ {
        s := <-results
        sum += s
        fmt.Println("sum", sum, "s", s)
    }   
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }   

    for j := 1; j <= 9; j++ {
        jobs <- j
    }   
    close(jobs)
    sumResult(results)
}

channel 问题

问题: [Is it OK to leave a channel open?

解答

It's OK to leave a Go channel open forever and never close it. When the channel is no longer used, it will be garbage collected.

Note that it is only necessary to close a channel if the receiver is looking for a close. Closing the channel is a control signal on the channel indicating that no more data follows.

参考资料

猜你喜欢

转载自blog.csdn.net/phantom_111/article/details/79489313