channel 是 golang 里相当有趣的一个功能,大部分时候 channel 都是和 goroutine 一起配合使用。本文主要介绍 channel 的一些有趣的用法。

通道(channel),像是通道(管道),可以通过它们发送类型化的数据在协程之间通信,可以避开所有内存共享导致的坑;通道的通信方式保证了同步性。数据通过通道:同一时间只有一个协程可以访问数据:所以不会出现数据竞争,设计如此。数据的归属(可以读写数据的能力)被传递。

通道实际上是类型化消息的队列:使数据得以传输。它是先进先出(FIFO)结构的所以可以保证发送给他们的元素的顺序(有些人知道,通道可以比作 Unix shells 中的双向管道(tw-way pipe))。通道也是引用类型,所以我们使用 make() 函数来给它分配内存。

二、Go Channel基本操作语法

Go Channel的基本操作语法如下:

c := make(chan bool) //创建一个无缓冲的bool型Channel

c <- x        //向一个Channel发送一个值
<- c          //从一个Channel中接收一个值
x = <- c      //从Channel c接收一个值并将其存储到x中
x, ok = <- c  //从Channel接收一个值,如果channel关闭了或没有数据,那么ok将被置为false

默认情况下,通信是同步且无缓冲的:在有接受者接收数据之前,发送不会结束。可以想象一个无缓冲的通道在没有空间来保存数据的时候:必须要一个接收者准备好接收通道的数据然后发送者可以直接把数据发送给接收者。所以通道的发送/接收操作在对方准备好之前是阻塞的:

1)对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的:如果ch中的数据无人接收,就无法再给通道传入其他数据:新的输入无法在通道非空的情况下传入。所以发送操作会等待 ch 再次变为可用状态:就是通道值被接收时(可以传入变量)。

2)对于同一个通道,接收操作是阻塞的(协程或函数中的),直到发送者可用:如果通道中没有数据,接收者就阻塞了。

三、Channel用作信号(Signal)的场景(信号量)

1、等待一个事件(Event)

等待一个事件。例如:

package main

import "fmt"

func main() {
        fmt.Println("Begin doing something!")
        c := make(chan bool)
        go func() {
                fmt.Println("Doing something…")
                close(c)
        }()
        <-c
        fmt.Println("Done!")
}

这里main goroutine通过"<-c"来等待sub goroutine中的“完成事件”,sub goroutine通过close channel促发这一事件。当然也可以通过向Channel写入一个bool值的方式来作为事件通知。main goroutine在channel c上没有任何数据可读的情况下会阻塞等待。

2、协同多个Goroutines

同上,close channel还可以用于协同多个Goroutines,比如下面这个例子,我们创建了100个Worker Goroutine,这些Goroutine在被创建出来后都阻塞在"<-start"上,直到我们在main goroutine中给出开工的信号:"close(start)",这些goroutines才开始真正的并发运行起来。

//testwaitevent2.go
package main

import "fmt"

func worker(start chan bool, index int) {
        <-start
        fmt.Println("This is Worker:", index)
}

func main() {
        start := make(chan bool)
        for i := 1; i <= 100; i++ {
                go worker(start, i)
        }
        close(start)
        select {} //deadlock we expected
}

3、Select

从不同的并发执行的协程中获取值可以通过关键字select来完成,它和switch控制语句非常相似(章节5.3)也被称作通信开关;它的行为像是“你准备好了吗”的轮询机制;select监听进入通道的数据,也可以是用通道发送值的时候。

select 做的就是:选择处理列出的多个通信情况中的一个。

  • 如果都阻塞了,会等待直到其中一个可以处理
  • 如果多个可以处理,随机选择一个
  • 如果没有通道操作可以处理并且写了 default 语句,它就会执行:default 永远是可运行的(这就是准备好了,可以执行)。

下面是select的基本操作。

select {
case x := <- somechan:
    // … 使用x进行一些操作

case y, ok := <- someOtherchan:
    // … 使用y进行一些操作,
    // 检查ok值判断someOtherchan是否已经关闭

case outputChan <- z:
    // … z值被成功发送到Channel上时

default:
    // … 上面case均无法通信时,执行此分支
}

我想这里John Graham-Cumming主要是想告诉我们select的default分支的实践用法。

1、select  for non-blocking receive

idle:= make(chan []byte, 5) //用一个带缓冲的channel构造一个简单的队列

select {
case b = <-idle:
 //尝试从idle队列中读取
    …
default:  //队列空,分配一个新的buffer
        makes += 1
        b = make([]byte, size)
}

2、select for non-blocking send

idle:= make(chan []byte, 5) //用一个带缓冲的channel构造一个简单的队列

select {
case idle <- b: //尝试向队列中插入一个buffer
        //…
default: //队列满?

}

【惯用法:for/select】

我们在使用select时很少只是对其进行一次evaluation,我们常常将其与for {}结合在一起使用,并选择适当时机从for{}中退出。

for {
        select {
        case x := <- somechan:
            // … 使用x进行一些操作

case y, ok := <- someOtherchan:
            // … 使用y进行一些操作,
            // 检查ok值判断someOtherchan是否已经关闭

case outputChan <- z:
            // … z值被成功发送到Channel上时

default:
            // … 上面case均无法通信时,执行此分支
        }
}

【终结workers】

下面是一个常见的终结sub worker goroutines的方法,每个worker goroutine通过select监视一个die channel来及时获取main goroutine的退出通知。

//testterminateworker1.go
package main

import (
    "fmt"
    "time"
)

func worker(die chan bool, index int) {
    fmt.Println("Begin: This is Worker:", index)
    for {
        select {
        //case xx:
            //做事的分支
        case <-die:
            fmt.Println("Done: This is Worker:", index)
            return
        }
    }
}

func main() {
    die := make(chan bool)

for i := 1; i <= 100; i++ {
        go worker(die, i)
    }

time.Sleep(time.Second * 5)
    close(die)
    select {} //deadlock we expected
}

【终结验证】

有时候终结一个worker后,main goroutine想确认worker routine是否真正退出了,可采用下面这种方法:

//testterminateworker2.go
package main

import (
    "fmt"
    //"time"
)

func worker(die chan bool) {
    fmt.Println("Begin: This is Worker")
    for {
        select {
        //case xx:
        //做事的分支
        case <-die:
            fmt.Println("Done: This is Worker")
            die <- true
            return
        }
    }
}

func main() {
    die := make(chan bool)

go worker(die)

die <- true
    <-die
    fmt.Println("Worker goroutine has been terminated")
}

【关闭的Channel永远不会阻塞】

通道可以被显式的关闭;尽管它们和文件不同:不必每次都关闭。只有在当需要告诉接收者不会再提供新的值的时候,才需要关闭通道。只有发送者需要关闭通道,接收者永远不会需要。

下面演示在一个已经关闭了的channel上读写的结果:

//testoperateonclosedchannel.go
package main

import "fmt"

func main() {
        cb := make(chan bool)
        close(cb)
        x := <-cb
        fmt.Printf("%#v\n", x)

x, ok := <-cb
        fmt.Printf("%#v %#v\n", x, ok)

ci := make(chan int)
        close(ci)
        y := <-ci
        fmt.Printf("%#v\n", y)

cb <- true
}

$go run testoperateonclosedchannel.go
false
false false
0
panic: runtime error: send on closed channel

可以看到在一个已经close的unbuffered channel上执行读操作,回返回channel对应类型的零值,比如bool型channel返回false,int型channel返回0。但向close的channel写则会触发panic。不过无论读写都不会导致阻塞。

【关闭带缓存的channel】

将unbuffered channel换成buffered channel会怎样?我们看下面例子:

//testclosedbufferedchannel.go
package main

import "fmt"

func main() {
        c := make(chan int, 3)
        c <- 15
        c <- 34
        c <- 65
        close(c)
        fmt.Printf("%d\n", <-c)
        fmt.Printf("%d\n", <-c)
        fmt.Printf("%d\n", <-c)
        fmt.Printf("%d\n", <-c)

c <- 1
}

$go run testclosedbufferedchannel.go
15
34
65
0
panic: runtime error: send on closed channel

可以看出带缓冲的channel略有不同。尽管已经close了,但我们依旧可以从中读出关闭前写入的3个值。第四次读取时,则会返回该channel类型的零值。向这类channel写入操作也会触发panic。

四、隐藏状态(自增长ID生成器)

下面通过一个例子来演示一下channel如何用来隐藏状态:

1、例子:唯一的ID服务

//testuniqueid.go
package main

import "fmt"

func newUniqueIDService() <-chan string {
        id := make(chan string)
        go func() {
                var counter int64 = 0
                for {
                        id <- fmt.Sprintf("%x", counter)
                        counter += 1
                }
        }()
        return id
}
func main() {
        id := newUniqueIDService()
        for i := 0; i < 10; i++ {
                fmt.Println(<-id)
        }
}

newUniqueIDService通过一个channel与main goroutine关联,main goroutine无需知道uniqueid实现的细节以及当前状态,只需通过channel获得最新id即可。

五、默认情况(最常见的方式:生产者/消费者)

生产者产生一些数据将其放入 channel;然后消费者按照顺序,一个一个的从 channel 中取出这些数据进行处理。这是最常见的 channel 的使用方式。当 channel 的缓冲用尽时,生产者必须等待(阻塞)。换句话说,若是 channel 中没有数据,消费者就必须等待了。

生产者

func producer(c chan int64, max int) {
defer
close(c) for i:= 0; i < max; i ++ {
c <- time.Now().Unix()
}
}

生产者生成“max”个 int64 的数字,并且将其放入 channel “c” 中。需要注意的是,这里用 defer 在函数推出的时候关闭了 channel。

消费者

func consumer(c chan int64) {
var v int64
ok := true for ok {
if v, ok = <-c; ok {
fmt.Println(v)
}
}
}

从 channel 中一个一个的读取 int64 的数字,然后将其打印在屏幕上。当 channel 被关闭后,变量“ok”将被设置为“false”。

六、Nil Channels

1、nil channels阻塞

对一个没有初始化的channel进行读写操作都将发生阻塞,例子如下:

package main

func main() {
        var c chan int
        <-c
}

$go run testnilchannel.go
fatal error: all goroutines are asleep – deadlock!

package main

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

$go run testnilchannel.go
fatal error: all goroutines are asleep – deadlock!

2、nil channel在select中很有用

看下面这个例子:

//testnilchannel_bad.go
package main

import "fmt"
import "time"

func main() {
        var c1, c2 chan int = make(chan int), make(chan int)
        go func() {
                time.Sleep(time.Second * 5)
                c1 <- 5
                close(c1)
        }()

go func() {
                time.Sleep(time.Second * 7)
                c2 <- 7
                close(c2)
        }()

for {
                select {
                case x := <-c1:
                        fmt.Println(x)
                case x := <-c2:
                        fmt.Println(x)
                }
        }
        fmt.Println("over")
}

我们原本期望程序交替输出5和7两个数字,但实际的输出结果却是:

5
0
0
0
… … 0死循环

再仔细分析代码,原来select每次按case顺序evaluate:
    – 前5s,select一直阻塞;
    – 第5s,c1返回一个5后被close了,“case x := <-c1”这个分支返回,select输出5,并重新select
    – 下一轮select又从“case x := <-c1”这个分支开始evaluate,由于c1被close,按照前面的知识,close的channel不会阻塞,我们会读出这个 channel对应类型的零值,这里就是0;select再次输出0;这时即便c2有值返回,程序也不会走到c2这个分支
    – 依次类推,程序无限循环的输出0

我们利用nil channel来改进这个程序,以实现我们的意图,代码如下:

//testnilchannel.go
package main

import "fmt"
import "time"

func main() {
        var c1, c2 chan int = make(chan int), make(chan int)
        go func() {
                time.Sleep(time.Second * 5)
                c1 <- 5
                close(c1)
        }()

go func() {
                time.Sleep(time.Second * 7)
                c2 <- 7
                close(c2)
        }()

for {
                select {
                case x, ok := <-c1:
                        if !ok {
                                c1 = nil
                        } else {
                                fmt.Println(x)
                        }
                case x, ok := <-c2:
                        if !ok {
                                c2 = nil
                        } else {
                                fmt.Println(x)
                        }
                }
                if c1 == nil && c2 == nil {
                        break
                }
        }
        fmt.Println("over")
}

$go run testnilchannel.go
5
7
over

可以看出:通过将已经关闭的channel置为nil,下次select将会阻塞在该channel上,使得select继续下面的分支evaluation。

七、Timers(超时定时器)

1、超时机制Timeout

带超时机制的select是常规的tip,下面是示例代码,实现30s的超时select:

func worker(start chan bool) {
        timeout := time.After(30 * time.Second)
        for {
                select {
                     // … do some stuff
                case <- timeout:
                    return
                }
        }
}

2、心跳HeartBeart

与timeout实现类似,下面是一个简单的心跳select实现:

func worker(start chan bool) {
        heartbeat := time.Tick(30 * time.Second)
        for {
                select {
                     // … do some stuff
                case <- heartbeat:
                    //… do heartbeat stuff
                }
        }
}

参考自:

http://blog.csdn.net/erlib/article/details/44097291

http://tonybai.com/2014/09/29/a-channel-compendium-for-golang/

Golang channel 用法简介的更多相关文章

  1. golang channel 用法转的

    一.Golang并发基础理论 Golang在并发设计方面参考了C.A.R Hoare的CSP,即Communicating Sequential Processes并发模型理论.但就像John Gra ...

  2. Golang Channel用法简编

    转自:http://tonybai.com/2014/09/29/a-channel-compendium-for-golang/ 在进入正式内容前,我这里先顺便转发一则消息,那就是Golang 1. ...

  3. Golang : pflag 包简介

    笔者在前文中介绍了 Golang 标准库中 flag 包的用法,事实上有一个第三方的命令行参数解析包 pflag 比 flag 包使用的更为广泛.pflag 包的设计目的就是替代标准库中的 flag ...

  4. IOS NSInvocation用法简介

    IOS NSInvocation用法简介 2012-10-25 19:59 来源:博客园 作者:csj007523 字号:T|T [摘要]在 iOS中可以直接调用某个对象的消息方式有两种,其中一种就是 ...

  5. JodaTime用法简介

    JodaTime用法简介 Java的Date和Calendar用起来简直就是灾难,跟C#的DateTime差距太明显了,幸好有JodaTime 本文简单罗列JodaTime的用法 package co ...

  6. Apache自带压力测试工具ab用法简介

    ab命令原理 ab命令会创建很多的并发访问线程,模拟多个访问者同时对某一URL进行访问.它的测试目标是基于URL的,因此,既可以用来测试Apache的负载压力,也可以测试nginx.lighthttp ...

  7. golang channel的使用以及调度原理

    golang channel的使用以及调度原理 为了并发的goroutines之间的通讯,golang使用了管道channel. 可以通过一个goroutines向channel发送数据,然后从另一个 ...

  8. Postman用法简介

    转自:http://blog.csdn.net/flowerspring/article/details/52774399 Postman用法简介 转载 2016年10月10日 09:04:10 10 ...

  9. Go基础系列:nil channel用法示例

    Go channel系列: channel入门 为select设置超时时间 nil channel用法示例 双层channel用法示例 指定goroutine的执行顺序 当未为channel分配内存时 ...

随机推荐

  1. [日常] nginx与location规则

    =========================================================================2018年3月28日 记录: location = / ...

  2. Myeclipse默认编码设置

    Myeclipse默认编码设置 由于编码问题的出现,我们就必须明确自己在Myeclipse所写的各种文件是按照什么格式来进行编码的.所以只有知己知彼,才能bug少出. 对于刚接触MyEclipse的不 ...

  3. HTTP协议之内容协商

    一个URL常常需要代表若干不同的资源.例如那种需要以多种语言提供其内容的网站站点.如果某个站点有说法语的和说英语的两种用户,它可能想用这两种语言提供网站站点信息.理想情况下,服务器应当向英语用户发送英 ...

  4. JSP学习笔记(3)-JSP内置对象

    有些对象不用声明就可以在JSP页面的Java程序片和表达式部分使用,这些对象就是JSP的内置对象.  JSP常用的内置对象有request,response,session,application,o ...

  5. java里 equals和== 区别

    1.java中equals和==的区别 值类型是存储在内存中的堆栈(简称栈),而引用类型的变量在栈中仅仅是存储引用类型变量的地址,而其本身则存储在堆中.2.==操作比较的是两个变量的值是否相等,对于引 ...

  6. cnpm 淘宝镜像设置

    很简单,一句话 npm install -g cnpm --registry=https://registry.npm.taobao.org

  7. join() 方法详解及应用场景

    总结:join方法的功能就是使异步执行的线程变成同步执行.也就是说,当调用线程实例的start方法后,这个方法会立即返回,如果在调用start方法后后需要使用一个由这个线程计算得到的值,就必须使用jo ...

  8. 异度之刃 Xenoblade 后感

    WII版重置的N3DS劣化版异度之刃终于通关了.在出色的自制系统的快乐NTR的帮助下,充分体验到了神作的剧情史诗感. 关于游戏的玩法系统,从现在来看8年前的游戏,缺点显而易见,特别是跑地图这回事,地图 ...

  9. 【Java】读写文本文件

    package rw; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileOutput ...

  10. java笔记--线程休眠sleep()的运用

    线程休眠sleep()方法的运用 在多线程编程中,有时需要让某个线程优先执行.除了可以设置这个线程的优先级为最高外,更加理想的方法是休眠其他线程,若有线程中断了正在休眠的线程,则抛出Interrupt ...