Go-Channel

问题

在前面讲goroutine的时候,自然会想到goroutine之间的同步问题。如果没有同步通信机制,那么goroutine的作用就非常有限了。

其他编程语言

Java的线程同步几乎没用过,这里不谈。
Go的同步机制,即本文将要描述的channel,和Python的pipe和类似;自然也和Linux C的piple一样,见Advanced Linux Programming P110 5.4 Pipe。——在Linux C中,和Introducing Go类似的入门级书籍,自然首推Advanced Linux Programming

注:这种轻量级的书籍对于扫盲是非常好的,C++的就如同Essential C++,薄而全,查阅方便,可以快速掌握概念。另一种风格的就是Head First系列。

示例

package main
import "fmt"
import "time"

func pinger(c chan string) {
    for i := 0; ; i++ {
        c <- "ping"
    }
}

func printer(c chan string) {
    for {
        msg := <- c
        fmt.Println(msg)
        time.Sleep(time.Second * 1)
    }
}

func waitKey() {
    var input string 
    fmt.Scanln(&input)
    fmt.Println("Entered:", input)
}

func main() {
    var c chan string = make(chan string)

    go pinger(c)
    go printer(c)

    waitKey()
}

代码分析

如果运行例子,会发现屏幕一直打印Ping字符串。

在代码中,创建了两个goroutine,一个是pinger,一个是printer。前者相当于生产者,后者相当于消费者。两者通过channel(即 c chan string)相连(相互通信)。pinger一直往channel中写入数据,而printer一直读取数据,并打印到屏幕上。——至此,我们可以认为到chan是一种新的数据类型,而具体到本例,这个channel放置的是string这种数据。

这里的main()调用了waitKey(),其原因在goroutine已经提到。

问题:
* channel是不是还可以放其他的数据类型呢?
* channel是不是通常所谓的生产者消费者模型,即pinger尽力生产(一直往channel中写数据),printer尽力消费。因为printer有sleep,那么会不会pinger生产的速度大于printer消费的速度?最后造成channel溢出?

为此,对代码做如下改造。

示例优化

package main
import "fmt"
import "time"

type UserDefinedData struct {
    msg string 
    id  int 
}

func pinger(c chan UserDefinedData) {
    data := UserDefinedData{"ping", 0}
    sentCounter := 0
    for i := 0; ; i++ {
        sentCounter++
        data.id = sentCounter 
        c <- data 

        fmt.Println("Sent count:", sentCounter)
    }
}

func printer(c chan UserDefinedData) {
    receivedCounter := 0
    for {
        receivedData := <- c
        fmt.Println(receivedData)

        receivedCounter++;
        fmt.Println("Received count:", receivedCounter)

        time.Sleep(time.Second * 1)
    }
}

func waitKey() {
    var input string 
    fmt.Scanln(&input)
    fmt.Println("Entered:", input)
}

func main() {
    var c chan UserDefinedData = make(chan UserDefinedData)

    go pinger(c)
    go printer(c)

    waitKey()
}

输出结果:

D:\examples>go run helloworld.go
{ping 1}
Received count: 1
Sent count: 1
{ping 2}
Received count: 2
Sent count: 2
{ping 3}
Received count: 3
Sent count: 3
{ping 4}
Received count: 4
Sent count: 4
{ping 5}
Received count: 5
Sent count: 5
{ping 6}
Received count: 6
Sent count: 6
{ping 7}
Received count: 7

结论:

  • channel只是一种通讯的通道,其中可以防止任何类型的数据。——当然同一channel,只能是任意固定的数据类型。(是不是对“任意固定”这个术语很熟悉?)
  • channel除了生产者消费者模型的含义,还具有同步的机制。P90: When pinger attempts to send a message on the channel, it will wait until printer is ready to receive the message (this is known as blocking).

至此,可以理解Introducing Go - Channel这一节的第一句话:

Channels provide a way for two goroutines to communicate with each other and synchronize their execution.

channel

至此,给出channel的定义。直接拷贝Introducing Go的内容:

  • A channel type is represented with the keyword chan followed by the type of the things that are passed on the channel (in this case, we are passing strings).
  • The left arrow operator (<-) is used to send and receive messages on the channel.

需要注意的是,这里只有”<-“一种操作符,没有所谓的”->”。但既可以用作send,也可以用作receive,这取决于channel对象放在那边。例如:

  • c <- “ping” means send “ping”.
  • msg := <- c means receive a message and store it in msg.

无厘头试验

进一步地,如果把代码改成下面的样子:

var c chan UserDefinedData = make(chan UserDefinedData)
var c2 chan UserDefinedData = make(chan UserDefinedData)

go pinger(c)
go printer(c2)

即pinger和printer使用不同的channel。亦或只有pinger一个goroutine:

var c chan UserDefinedData = make(chan UserDefinedData)
//var c2 chan UserDefinedData = make(chan UserDefinedData)

go pinger(c)
//go printer(c2)

如果运行,发现pinger&printer都没有打印输出。这是否可以得到另一个结论:如果channel没有对应的输入(生产者)或输出(消费者),则channel不工作。为此,只需要重读下面一句话(前面已经提及过):

When pinger attempts to send a message on the channel, it will wait until printer is ready to receive the message (this is known as blocking).

m-n映射关系

前面讲到了channel必须有一个生产者和一个消费者。接下来的问题是,一个channel是否只能对应一个生产者或消费者。

为此,代码改成下面的样子(打印顺序有调整)。

package main
import "fmt"
import "time"

type UserDefinedData struct {
    msg string 
    id  int 
}

func sender(c chan UserDefinedData, id string) {
    data := UserDefinedData{id, 0}
    sentCounter := 0
    for i := 0; ; i++ {
        sentCounter++
        data.id = sentCounter 

        fmt.Println("[", id, "]Sent count:", sentCounter)
        c <- data 
    }
}

func receiver(c chan UserDefinedData, id string) {
    receivedCounter := 0
    for {
        receivedData := <- c
        fmt.Println("[", id, "]", receivedData)

        receivedCounter++;
        fmt.Println("[", id, "]Received count:", receivedCounter)

        time.Sleep(time.Second * 1)
    }
}

func waitKey() {
    var input string 
    fmt.Scanln(&input)
    fmt.Println("Entered:", input)
}

func main() {
    var c chan UserDefinedData = make(chan UserDefinedData)

    go sender(c, "sender-1")
    go sender(c, "sender-2")
    go receiver(c, "receiver-1")
    go receiver(c, "receiver-2")

    waitKey()
}

即现在为channel绑定了2个sender和2个receiver(函数名称做了优化)。运行时的(一种)打印:

D:\examples>go run helloworld.go
[ sender-1 ]Sent count: 1
[ sender-2 ]Sent count: 1
[ receiver-1 ] {sender-1 1}
[ receiver-1 ]Received count: 1
[ sender-2 ]Sent count: 2
[ sender-1 ]Sent count: 2
[ receiver-2 ] {sender-2 1}
[ receiver-2 ]Received count: 1
[ receiver-1 ] {sender-2 2}
[ receiver-1 ]Received count: 2
[ sender-2 ]Sent count: 3
[ receiver-2 ] {sender-1 2}
[ receiver-2 ]Received count: 2
[ sender-1 ]Sent count: 3
[ receiver-1 ] {sender-2 3}
[ receiver-1 ]Received count: 3
[ sender-2 ]Sent count: 4
[ receiver-2 ] {sender-1 3}
[ receiver-2 ]Received count: 3
[ sender-1 ]Sent count: 4
[ receiver-1 ] {sender-2 4}
[ receiver-1 ]Received count: 4
[ sender-2 ]Sent count: 5
[ receiver-2 ] {sender-1 4}
[ receiver-2 ]Received count: 4
[ sender-1 ]Sent count: 5
[ sender-2 ]Sent count: 6
[ receiver-1 ] {sender-2 5}
[ receiver-1 ]Received count: 5
[ receiver-2 ] {sender-1 5}
[ receiver-2 ]Received count: 5
[ sender-1 ]Sent count: 6
Entered:
exit status 2

D:\examples>

看着打印稍微有些乱,所以捋一捋:

//sender-1 -> receiver-1
[ sender-1 ]Sent count: 1
[ receiver-1 ] {sender-1 1}
[ receiver-1 ]Received count: 1

//sender-2 -> receiver-2
[ sender-2 ]Sent count: 1
[ receiver-2 ] {sender-2 1}
[ receiver-2 ]Received count: 1

//sender-2 -> receiver-1
[ sender-2 ]Sent count: 2
[ receiver-1 ] {sender-2 2}
[ receiver-1 ]Received count: 2

//sender-1 -> receiver-2
[ sender-1 ]Sent count: 2
[ receiver-2 ] {sender-1 2}
[ receiver-2 ]Received count: 2

//sender-2 -> receiver-1
[ sender-2 ]Sent count: 3
[ receiver-1 ] {sender-2 3}
[ receiver-1 ]Received count: 3

//sender-1 -> receiver-2
[ sender-1 ]Sent count: 3
[ receiver-2 ] {sender-1 3}
[ receiver-2 ]Received count: 3

[ sender-2 ]Sent count: 4
[ receiver-1 ] {sender-2 4}
[ receiver-1 ]Received count: 4

[ sender-1 ]Sent count: 4
[ receiver-2 ] {sender-1 4}
[ receiver-2 ]Received count: 4

[ sender-2 ]Sent count: 5
[ receiver-1 ] {sender-2 5}
[ receiver-1 ]Received count: 5

[ sender-1 ]Sent count: 5
[ receiver-2 ] {sender-1 5}
[ receiver-2 ]Received count: 5

[ sender-2 ]Sent count: 6
[ sender-1 ]Sent count: 6
...

结论如下:
- 如果一个channel绑定了多个sender或多个receiver,那么sender和receiver之间的连接关系是随机的。

如果注释掉一个receiver,比如:

//go receiver(c, "receiver-2")

则打印为:

D:\examples>go run helloworld.go
[ sender-1 ]Sent count: 1
[ sender-2 ]Sent count: 1
[ receiver-1 ] {sender-1 1}
[ receiver-1 ]Received count: 1
[ sender-1 ]Sent count: 2
[ receiver-1 ] {sender-2 1}
[ receiver-1 ]Received count: 2
[ sender-2 ]Sent count: 2
[ receiver-1 ] {sender-1 2}
[ receiver-1 ]Received count: 3
[ sender-1 ]Sent count: 3
[ receiver-1 ] {sender-2 2}
[ receiver-1 ]Received count: 4
[ sender-2 ]Sent count: 3
[ receiver-1 ] {sender-1 3}
[ receiver-1 ]Received count: 5
[ sender-1 ]Sent count: 4
[ receiver-1 ] {sender-2 3}
[ receiver-1 ]Received count: 6
[ sender-2 ]Sent count: 4
[ sender-1 ]Sent count: 5
[ receiver-1 ] {sender-1 4}
[ receiver-1 ]Received count: 7
[ receiver-1 ] {sender-2 4}
[ receiver-1 ]Received count: 8
[ sender-2 ]Sent count: 5
[ receiver-1 ] {sender-1 5}
[ receiver-1 ]Received count: 9
[ sender-1 ]Sent count: 6
[ receiver-1 ] {sender-2 5}
[ receiver-1 ]Received count: 10
[ sender-2 ]Sent count: 6
[ receiver-1 ] {sender-1 6}
[ receiver-1 ]Received count: 11
[ sender-1 ]Sent count: 7
[ receiver-1 ] {sender-2 6}
[ receiver-1 ]Received count: 12
[ sender-2 ]Sent count: 7
Entered:
exit status 2

D:\examples>

异步

多线程通信、进程间通信都涉及到同步和异步的问题。在前面的代码中,用到的都是同步,如前面提到的blocking。Go有种变相的“异步”(其实不彻底)实现机制,即为channel设置一个缓冲区大小。缺省情况下,缓冲区大小为1。在sender断开了,发出的数据只要没有被receiver接收,它就不能继续发送。如果缓冲区设置为n,那么sender就可以连续发送至多n个到缓冲区,或者说只要缓冲区的数据量少于n个,sender就可以发送。

示例代码

为了拷贝方便,代码重复如下:——主要是main部分只变量了sender-1和receiver-1.

package main
import "fmt"
import "time"

type UserDefinedData struct {
    msg string 
    id  int 
}

func sender(c chan UserDefinedData, id string) {
    data := UserDefinedData{id, 0}
    sentCounter := 0
    for i := 0; ; i++ {
        sentCounter++
        data.id = sentCounter 

        fmt.Println("[", id, "]Sent count:", sentCounter)
        c <- data 
    }
}

func receiver(c chan UserDefinedData, id string) {
    receivedCounter := 0
    for {
        receivedData := <- c
        fmt.Println("[", id, "]", receivedData)

        receivedCounter++;
        fmt.Println("[", id, "]Received count:", receivedCounter)

        time.Sleep(time.Second * 1)
    }
}

func waitKey() {
    var input string 
    fmt.Scanln(&input)
    fmt.Println("Entered:", input)
}

func main() {
    var c chan UserDefinedData = make(chan UserDefinedData, 3)

    go sender(c, "sender-1")
    go receiver(c, "receiver-1")

    waitKey()
}

运行结果

D:\examples>go run helloworld.go
[ sender-1 ]Sent count: 1
[ sender-1 ]Sent count: 2
[ sender-1 ]Sent count: 3
[ sender-1 ]Sent count: 4
[ receiver-1 ] {sender-1 1}
[ receiver-1 ]Received count: 1
[ sender-1 ]Sent count: 5
[ receiver-1 ] {sender-1 2}
[ receiver-1 ]Received count: 2
[ sender-1 ]Sent count: 6
[ receiver-1 ] {sender-1 3}
[ receiver-1 ]Received count: 3
[ sender-1 ]Sent count: 7
[ receiver-1 ] {sender-1 4}
[ receiver-1 ]Received count: 4
[ sender-1 ]Sent count: 8
[ receiver-1 ] {sender-1 5}
[ receiver-1 ]Received count: 5
[ sender-1 ]Sent count: 9
[ receiver-1 ] {sender-1 6}
[ receiver-1 ]Received count: 6
[ sender-1 ]Sent count: 10
[ receiver-1 ] {sender-1 7}
[ receiver-1 ]Received count: 7
[ sender-1 ]Sent count: 11
Entered:
exit status 2

D:\examples>

sender连续发送1/2/3/4,并非是指receiver还没有接收的情况下可以往缓冲区发送4个。事实上,receiver已经接收了一个,但fmt.Println延时。——涉及到多线程多进程的调试问题。

direction & select

接下来简要描述channel的两位两个特性。

direction

可以指定channel是双向的还是单向的,是只能发送、还是只能接收。在前面的示例代码中,缺省都是双向。下面是单向channel的使用示例:

func pinger(c chan<- string)
func printer(c <-chan string)
  • chan<-: sender;此时不能接收数据。
  • <-chan: receiver; 此时不能发送数据。

建议在代码中明确方向,以提高代码可读性。

select

前面讨论了一个channel上面的m-n映射关系。这里的select则是多个sender+channel,一个receiver。对于receiver,Go提供了一种机制可以识别出来自于哪个sender。此即select语法。

代码

package main
import "fmt"
import "time"

type UserDefinedData struct {
    msg string 
    id  int 
}

func sender(c chan UserDefinedData, id string) {
    data := UserDefinedData{id, 0}
    sentCounter := 0
    for i := 0; ; i++ {
        sentCounter++
        data.id = sentCounter 

        fmt.Println("[", id, "]Sent count:", sentCounter)
        c <- data 
    }
}

func receiver(c1 chan UserDefinedData, c2 chan UserDefinedData, id string) {
    receivedCounter := 0
    for {
        select {
        case receivedData1 := <- c1:
            fmt.Println("[", id, "] received data from c1:", receivedData1)
        case receivedData2 := <- c2:
            fmt.Println("[", id, "] received data from c2:", receivedData2)
        }

        receivedCounter++;
        fmt.Println("[", id, "]Received count:", receivedCounter)

        time.Sleep(time.Second * 1)
    }
}

func waitKey() {
    var input string 
    fmt.Scanln(&input)
    fmt.Println("Entered:", input)
}

func main() {
    var c1 chan UserDefinedData = make(chan UserDefinedData)
    var c2 chan UserDefinedData = make(chan UserDefinedData)

    go sender(c1, "sender-1")
    go sender(c2, "sender-2")
    go receiver(c1, c2, "receiver-1")
    //go receiver(c1, c2, "receiver-2")

    waitKey()
}

运行结果

D:\examples>go run helloworld.go
[ sender-1 ]Sent count: 1
[ sender-2 ]Sent count: 1
[ receiver-1 ] received data from c2: {sender-2 1}
[ receiver-1 ]Received count: 1
[ sender-2 ]Sent count: 2
[ receiver-1 ] received data from c2: {sender-2 2}
[ receiver-1 ]Received count: 2
[ sender-2 ]Sent count: 3
[ receiver-1 ] received data from c2: {sender-2 3}
[ receiver-1 ]Received count: 3
[ sender-2 ]Sent count: 4
[ receiver-1 ] received data from c2: {sender-2 4}
[ receiver-1 ]Received count: 4
[ sender-2 ]Sent count: 5
[ receiver-1 ] received data from c1: {sender-1 1}
[ sender-1 ]Sent count: 2
[ receiver-1 ]Received count: 5
[ receiver-1 ] received data from c1: {sender-1 2}
[ receiver-1 ]Received count: 6
[ sender-1 ]Sent count: 3
[ receiver-1 ] received data from c1: {sender-1 3}
[ receiver-1 ]Received count: 7
[ sender-1 ]Sent count: 4
[ receiver-1 ] received data from c1: {sender-1 4}
[ receiver-1 ]Received count: 8
[ sender-1 ]Sent count: 5
Entered:

D:\examples>

Further More

可以把main()中的下面注释打开,观察并分析运行结果:

//go receiver(c1, c2, "receiver-2")

小结

要点如下:

  • channel为goroutine提供了一种通信机制
  • channel缺省为双向通信,也可以指定<-chan或chan<-以明确单一的方向
  • channel缺省是阻塞式通信
  • channel缺省capability为1,可以make(chan, capability)实现异步通信
  • receiver可以通过select确定使用的是哪个channel
阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页