Golang教程:goroutine信道

Posted 奔梦

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang教程:goroutine信道相关的知识,希望对你有一定的参考价值。

在上一篇教程中,我们讨论了如何使用协程实现并发。在这篇教程中,我们将讨论信道以及如何使用信道实现协程间通信。

什么是信道

信道(Channel)可以被认为是协程之间通信的管道。与水流从管道的一端流向另一端一样,数据可以从信道的一端发送并在另一端接收。

声明信道

每个信道都有一个与之关联的类型。此类型是允许信道传输的数据类型,除此类型外不能通过信道传输其他类型。

chan T 是一个 T 类型的信道。

信道的 0 值为 nil。值为 nil 的信道变量没有任何用处,我们需要通过内置函数 make 来创建一个信道,就像创建map和 slice一样。

下面的代码声明了一个信道:

 1 package main
 2 
 3 import "fmt"
 4 
 5 func main() {  
 6     var a chan int
 7     if a == nil {
 8         fmt.Println("channel a is nil, going to define it")
 9         a = make(chan int)
10         fmt.Printf("Type of a is %T", a)
11     }
12 }

因为信道的 0 值为 nil,因此第 6 行声明的信道 a 的值为 nil。因此执行 if 里面的语句创建信道。上面的程序中 a 是一个 int 类型的信道。程序的输出为:

channel a is nil, going to define it  
Type of a is chan int 

像往常一样,速记声明也是定义信道的一种有效而简洁的方式:

a := make(chan int) 

上面的这行代码同样定义了一个 int 型的信道。

通过信道发送和接收数据

通过信道发送和接收数据的语法如下:

data := <- a // read from channel a  
a <- data // write to channel a  

箭头的指向说明了数据是发送还是接收。

在第一行,箭头的方向是从 a 向外指,因此我们正在从信道 a 中读取数据并将读取的值赋值给变量 data 。

在第二行,箭头的方式是指向 a ,因此我们正在向信道 a 中写入数据。

发送和接收默认是阻塞的

通过信道发送和接收数据默认是阻塞的。这是什么意思呢?当数据发送给信道后,程序流程在发送语句处阻塞,直到其他协程从该信道中读取数据。同样地,当从信道读取数据时,程序在读取语句处阻塞,直到其他协程发送数据给该信道。

信道的这种特性使得协程间通信变得高效,而不是向其他编程语言一样,显式的使用锁和条件变量来达到此目的。

信道的一个例子

理论到此为止:) 让我们通过一个程序来理解协程之间如何使用信道进行通信。

我们将用信道来重写在上一篇教程中的一个例子。

如下是那篇教程中的一个例子:

package main

import (  
    "fmt"
    "time"
)

func hello() {  
    fmt.Println("Hello world goroutine")
}
func main() {  
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}

这是上一篇教程中的例子,我们通过使用 Sleep 来使主协程休眠,以等待 hello 协程执行结束。如果你不明白这是为什么,请阅读上一篇教程

我们用信道重写上面的程序,如下:

 1 package main
 2 
 3 import (  
 4     "fmt"
 5 )
 6 
 7 func hello(done chan bool) {  
 8     fmt.Println("Hello world goroutine")
 9     done <- true
10 }
11 func main() {  
12     done := make(chan bool)
13     go hello(done)
14     <-done
15     fmt.Println("main function")
16 }

在上面的程序中,我们在第 12 行定义了一个 bool 类型的信道 done,然后将它作为参数传递给 hello 协程。在第 14 行,我们从信道 done 中读取数据。程序将在这一行被阻塞直到其他协程向信道 done 里写入数据,在未读取到数据之前程序将在这一行一直等待而不会执行下一行语句。因此这里消除了在原程序中使用 time.Sleep 来阻止主协程退出的必要。

<-done 这一行从信道 done 中读取数据,但是没有使用该数据,也没有将它赋值给其他变量,这是完全合法的。

现在我们的 main 协程被阻塞,等待从信道 done 中读取数据。hello 协程接受信道 done 作为参数,打印 Hello world goroutine 然后将数据写入信道 done 中。当写入完毕后,main 协程从信道 done 中接收到数据,main 协程解除阻塞,继续执行下一条语句,打印:main function

程序的输出为:

Hello world goroutine  
main function  

让我们修改上面程序,在 hello 协程中加入一个休眠,来更好的理解阻塞的概念。

 1 package main
 2 
 3 import (  
 4     "fmt"
 5     "time"
 6 )
 7 
 8 func hello(done chan bool) {  
 9     fmt.Println("hello go routine is going to sleep")
10     time.Sleep(4 * time.Second)
11     fmt.Println("hello go routine awake and going to write to done")
12     done <- true
13 }
14 func main() {  
15     done := make(chan bool)
16     fmt.Println("Main going to call hello go goroutine")
17     go hello(done)
18     <-done
19     fmt.Println("Main received data")
20 }

在上面程序中的第 10 行,我们在 hello 函数中增加了4 秒钟的休眠。

该程序首先打印 Main going to call hello go goroutine 。然后 hello 协程开始执行,它将打印 hello go routine is going to sleep,然后 hello 协程休眠 4 秒,在这期间, main 协程由于在等待从信道 done 中读取数据而始终阻塞(在<-done 这一行)。4 秒中之后, hello 协程打印:hello go routine awake and going to write to don,接着 main 协程打印:Main received data 。

信道的另一个例子

让我们再写一个例子来更好的理解信道。该程序打印一个数字的每一位的平方和与立方和,并将平方和与立方和相加得出最后的结果。

例如,输入123 ,程序将做如下计算以得出最后结果:

squares = (1 * 1) + (2 * 2) + (3 * 3) 
cubes = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3) 
output = squares + cubes = 49

我们将平方和的计算与立方和的计算分别放在一个协程中执行,最后在主协程中将它们的计算结果求和。

 1 package main
 2 
 3 import (  
 4     "fmt"
 5 )
 6 
 7 func calcSquares(number int, squareop chan int) {  
 8     sum := 0
 9     for number != 0 {
10         digit := number % 10
11         sum += digit * digit
12         number /= 10
13     }
14     squareop <- sum
15 }
16 
17 func calcCubes(number int, cubeop chan int) {  
18     sum := 0 
19     for number != 0 {
20         digit := number % 10
21         sum += digit * digit * digit
22         number /= 10
23     }
24     cubeop <- sum
25 } 
26 
27 func main() {  
28     number := 589
29     sqrch := make(chan int)
30     cubech := make(chan int)
31     go calcSquares(number, sqrch)
32     go calcCubes(number, cubech)
33     squares, cubes := <-sqrch, <-cubech
34     fmt.Println("Final output", squares + cubes)
35 }

在第 7 行,函数 calcSquares 计算 number 每一位的平方和,并将结果发送给信道 squareop。同样地,在第 17 行,函数calcCubes 计算 number 每一位的立方和,并将结果发送给信道 cubeop

这两个函数接受不同的信道作为参数,并分别运行在各自的协程中(第31行和32行),最后将结果写入各自的信道。主协程在第 33 行同时等待这两个信道中的数据。一旦从这两个信道中接收到数据,它们分别被存放在变量 squares 和 cubes中,最后将它们的和打印出来。程序的输出为:

Final output 1536  

 

以上是关于Golang教程:goroutine信道的主要内容,如果未能解决你的问题,请参考以下文章

在C#中使用类golang信道编程

goroutine简介

golang语言并发与并行——goroutine和channel的详细理解

Go语言的并发(多线程协程)通道(信道)缓冲信道(Buffer Channels)长度和容量

Golang教程:goroutine协程

GoLang协程与通道---上