问题
在前面讲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
有疑问加站长微信联系(非本文作者)