网站首页 返回列表 像“草根”一样,紧贴着地面,低调的存在,冬去春来,枯荣无恙。 Golang学习笔记之初识并发特性(下) 23-09-20 14:51:49 字节波 358 接着上一章节的学习,来到了Go语言的灵魂特性Channel,我们认真学习了Go协程之后,最重点的应该是需要利用Channel来完成Go协程之间的通信了。如上一章节讲到的当需要在其他协程结束执行之前,阻塞Go主协程,我们利用`sleep`方法是不可取的,而Channel可以更优雅的完成该工作,本章节会逐步剖析Channel。 ### 信道(Channel)是什么 信道可以想像成 Go 协程之间通信的管道。如同管道中的水会从一端流到另一端,通过使用信道,数据也可以从一端发送,在另一端接收。 ### 信道的声明 - 所有信道都关联了一个类型。信道只能运输这种类型的数据,而运输其他类型的数据都是非法的。 - `chan T`表示T类型的信道。 - 信道的零值为nil。信道的零值没有什么用,应该像对map和切片所做的那样,用`make`来定义信道。 下面编写代码,声明一个信道: ```go package main import "fmt" func main() { var a chan int if a == nil { fmt.Println("channel a is nil, going to define it") a = make(chan int) fmt.Printf("Type of a is %T", a) } } ``` 输出: ```shell channel a is nil, going to define it Type of a is chan int ``` 由于信道的零值为nil,在第6行,信道a的值就是nil。于是,程序执行了if语句内的语句,定义了信道a。程序中a是一个int类型的信道。 简短声明通常也是一种定义信道的简洁有效的方法。 ```go a := make(chan int) ``` 这一行代码同样定义了一个int类型的信道a。 ### 通过信道进行发送和接收 如下所示,该语法通过信道发送和接收数据。 ```go data := <- a // 读取信道a a <- data // 写入信道a ``` 信道旁的箭头方向指定了是发送数据还是接收数据。 在第一行,箭头对于a来说是向外指的,因此我们读取了信道a的值,并把该值存储到变量data。 在第二行,箭头指向了a,因此我们在把数据写入信道a。 ### 发送与接收默认是阻塞的 发送与接收默认是阻塞的。这是什么意思?当把数据发送到信道时,程序控制会在发送数据的语句处发生阻塞,直到有其它Go协程从信道读取到数据,才会解除阻塞。与此类似,当读取信道的数据时,如果没有其它的协程把数据写入到这个信道,那么读取过程就会一直阻塞着。 信道的这种特性能够帮助Go协程之间进行高效的通信,不需要用到其他编程语言常见的显式锁或条件变量。 ### 信道的代码示例 回顾上章学习Go协程时写的程序 ```go 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") } ``` 我们接下来使用信道来重写上面代码。 ```go package main import ( "fmt" ) func hello(done chan bool) { fmt.Println("Hello world goroutine") done <- true } func main() { done := make(chan bool) go hello(done) <-done fmt.Println("main function") } ``` 在上述程序里,我们在第12行创建了一个bool类型的信道`done`,并把`done`作为参数传递给了`hello`协程。在第14行,我们通过信道`done`接收数据。这一行代码发生了阻塞,除非有协程向`done`写入数据,否则程序不会跳到下一行代码。于是,这就不需要用以前的`time.Sleep`来阻止Go主协程退出了。 `<-done`这行代码通过协程接收信道`done`数据,但并没有使用数据或者把数据存储到变量中。这完全是合法的。 现在我们的Go主协程发生了阻塞,等待信道`done`发送的数据。该信道作为参数传递给了协程`hello`,`hello`打印出`Hello world goroutine`,接下来向`done`写入数据。当完成写入时,Go主协程会通过信道`done`接收数据,于是它解除阻塞状态,打印出文本 `main function`。 该程序输出: ```shell Hello world goroutine main function ``` 当然了,我们常在测试时,为了更容易理解程序运行过程,会经常用到sleep方法,我们稍微修改一下程序,以便更好地理解阻塞的概念。 ```go package main import ( "fmt" "time" ) func hello(done chan bool) { fmt.Println("hello go routine is going to sleep") time.Sleep(4 * time.Second) fmt.Println("hello go routine awake and going to write to done") done <- true } func main() { done := make(chan bool) fmt.Println("Main going to call hello go goroutine") go hello(done) <-done fmt.Println("Main received data") } ``` 在上面程序里,我们向`hello`函数里添加了4秒的休眠(第10行)。 程序首先会打印`Main going to call hello go goroutine`。接着会开启`hello`协程,打印`hello go routine is going to sleep`。打印完之后,`hello`协程会休眠4秒钟,而在这期间,主协程会在`<-done`这一行发生阻塞,等待来自信道`done`的数据。4秒钟之后,打印`hello go routine awake and going to write to done`,接着再打印`Main received data`。 输出: ```shell Main going to call hello go goroutine hello go routine is going to sleep hello go routine awake and going to write to done Main received data ``` ### 信道的另一个示例 我们再编写一个程序来更好地理解信道。该程序会计算一个数中每一位的平方和与立方和,然后把平方和与立方和相加并打印出来。 例如,如果输出是123,该程序会如下计算输出: ```shell squares = (1 * 1) + (2 * 2) + (3 * 3) cubes = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3) output = squares + cubes = 50 ``` 我们会这样去构建程序:在一个单独的Go协程计算平方和,而在另一个协程计算立方和,最后在Go主协程把平方和与立方和相加。 ```go package main import ( "fmt" ) func calcSquares(number int, squareop chan int) { sum := 0 for number != 0 { digit := number % 10 sum += digit * digit number /= 10 } squareop <- sum } func calcCubes(number int, cubeop chan int) { sum := 0 for number != 0 { digit := number % 10 sum += digit * digit * digit number /= 10 } cubeop <- sum } func main() { number := 589 sqrch := make(chan int) cubech := make(chan int) go calcSquares(number, sqrch) go calcCubes(number, cubech) squares, cubes := <-sqrch, <-cubech fmt.Println("Final output", squares + cubes) } ``` 在第7行,函数`calcSquares`计算一个数每位的平方和,并把结果发送给信道`squareop`。与此类似,在第17行函数`calcCubes`计算一个数每位的立方和,并把结果发送给信道`cubop`。 这两个函数分别在单独的协程里运行(第31行和第32行),每个函数都有传递信道的参数,以便写入数据。Go主协程会在第33行等待两个信道传来的数据。一旦从两个信道接收完数据,数据就会存储在变量`squares`和`cubes`里,然后计算并打印出最后结果。该程序会输出: ```shell Final output 1536 ``` ### 死锁 使用信道需要考虑的一个重点是死锁。当Go协程给一个信道发送数据时,照理说会有其他Go协程来接收数据。如果没有的话,程序就会在运行时触发panic,形成死锁。 同理,当有Go协程等着从一个信道接收数据时,我们期望其他的Go协程会向该信道写入数据,要不然程序就会触发panic。 ```go package main func main() { ch := make(chan int) ch <- 5 } ``` 在上述程序中,我们创建了一个信道`ch`,接着在下一行`ch <- 5`,我们把`5`发送到这个信道。对于本程序,没有其他的协程从`ch`接收数据。于是程序触发panic,出现如下运行时错误。 ```shell fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main() D:/golang/1/test2.go:5 +0x57 ``` ### 单向信道 我们目前讨论的信道都是双向信道,即通过信道既能发送数据,又能接收数据。其实也可以创建单向信道,这种信道只能发送或者接收数据。 ```go package main import "fmt" func sendData(sendch chan<- int) { sendch <- 10 } func main() { sendch := make(chan<- int) go sendData(sendch) fmt.Println(<-sendch) } ``` 上面程序的第10行,我们创建了唯送(Send Only)信道`sendch`。`chan<- int`定义了唯送信道,因为箭头指向了`chan`。在第12行,我们试图通过唯送信道接收数据,于是编译器报错: ```shell invalid operation: <-sendch (receive from send-only type chan<- int) ``` 一切都很顺利,只不过一个不能读取数据的唯送信道究竟有什么意义呢? 这就需要用到信道转换(Channel Conversion)了。把一个双向信道转换成唯送信道或者唯收(Receive Only)信道都是行得通的,但是反过来就不行。 ```go package main import "fmt" func sendData(sendch chan<- int) { sendch <- 10 } func main() { cha1 := make(chan int) go sendData(cha1) fmt.Println(<-cha1) } ``` 在上述程序的第10行,我们创建了一个双向信道`cha1`。在第11行`cha1`作为参数传递给了`sendData`协程。在第5行,函数`sendData`里的参数`sendch chan<- int`把`cha1`转换为一个唯送信道。于是该信道在`sendData`协程里是一个唯送信道,而在Go主协程里是一个双向信道。该程序最终打印输出`10`。 ### 关闭信道和使用 for range 遍历信道 数据发送方可以关闭信道,通知接收方这个信道不再有数据发送过来。 当从信道接收数据时,接收方可以多用一个变量来检查信道是否已经关闭。 ```go v, ok := <- ch ``` 上面的语句里,如果成功接收信道所发送的数据,那么`ok`等于 true。而如果`ok`等于false,说明我们试图读取一个关闭的通道。从关闭的信道读取到的值会是该信道类型的零值。例如,当信道是一个`int`类型的信道时,那么从关闭的信道读取的值将会是`0`。 ```go package main import ( "fmt" ) func producer(chnl chan int) { for i := 0; i < 10; i++ { chnl <- i } close(chnl) } func main() { ch := make(chan int) go producer(ch) for { v, ok := <-ch if ok == false { break } fmt.Println("Received ", v, ok) } } ``` 在上述的程序中,`producer`协程会从0到9写入信道`chn1`,然后关闭该信道。主函数有一个无限的for循环(第16行),使用变量`ok`(第18行)检查信道是否已经关闭。如果`ok`等于false,说明信道已经关闭,于是退出for循环。如果`ok`等于true,会打印出接收到的值和`ok`的值。 ```shell Received 0 true Received 1 true Received 2 true Received 3 true Received 4 true Received 5 true Received 6 true Received 7 true Received 8 true Received 9 true ``` for range 循环用于在一个信道关闭之前,从信道接收数据。 接下来我们使用 for range 循环重写上面的代码。 ```go package main import ( "fmt" ) func producer(chnl chan int) { for i := 0; i < 10; i++ { chnl <- i } close(chnl) } func main() { ch := make(chan int) go producer(ch) for v := range ch { fmt.Println("Received ",v) } } ``` 在第16行,for range循环从信道`ch`接收数据,直到该信道关闭。一旦关闭了`ch`,循环会自动结束。该程序会输出: ```shell Received 0 Received 1 Received 2 Received 3 Received 4 Received 5 Received 6 Received 7 Received 8 Received 9 ``` 我们可以使用for range循环,重写信道的另一个示例这一节里面的代码,提高代码的可重用性。 最后,我们再来回顾一下刚刚计算平方与立方和的例子,如果你仔细观察这段代码,会发现获得一个数里的每位数`digit`的计算在`calcSquares`和`calcCubes`两个函数内多次重复。我们将把这段代码抽离出来,放在一个单独的函数里,然后并发地调用它。 ```go package main import ( "fmt" ) func digits(number int, dchnl chan int) { for number != 0 { digit := number % 10 dchnl <- digit number /= 10 } close(dchnl) } func calcSquares(number int, squareop chan int) { sum := 0 dch := make(chan int) go digits(number, dch) for digit := range dch { sum += digit * digit } squareop <- sum } func calcCubes(number int, cubeop chan int) { sum := 0 dch := make(chan int) go digits(number, dch) for digit := range dch { sum += digit * digit * digit } cubeop <- sum } func main() { number := 589 sqrch := make(chan int) cubech := make(chan int) go calcSquares(number, sqrch) go calcCubes(number, cubech) squares, cubes := <-sqrch, <-cubech fmt.Println("Final output", squares+cubes) } ``` 上述程序里的`digits`函数,包含了获取一个数的每位数的逻辑,并且`calcSquares`和`calcCubes`两个函数并发地调用了`digits`。当计算完数字里面的每一位数时,第13行就会关闭信道。`calcSquares`和`calcCubes`两个协程使用for range循环分别监听了它们的信道,直到该信道关闭。程序的其他地方不变,该程序同样会输出: ```shell Final output 1536 ``` 关键字词[Golang, Goroutine, Channel] 分享到: 上一篇:Golang学习笔记之初识并发特性(上) 下一篇:Golang学习笔记之五大阶段 如需留言,请 登录,没有账号?请 注册 0 条评论 0 人参与 最新文章 Dapp合约开发指南 ansible学习记录-远程开启exe不能挂起UI界面 leetcode基础算法学习之maxArea leetcode基础算法学习之ReverseInt leetcode基础算法学习之LongestSubstr leetcode基础算法学习之addTwoNumbers leetcode基础算法学习之FindIndex CentOS7安装nginx服务 点击排行 优雅的语言开发优雅的站点 Beego框架第1节——环境与初始 Golang学习笔记之匿名函数与闭包 Golang学习笔记之interface Dapp合约开发指南 最新评论 字节波 官方 1年前 你好,可以,麻烦你的站点做好友链 字节波 官方 1年前 欢迎各界人士评论留言,注意要遵守法律法规,祝每一位... 友情链接 BYTE STUDIO 字节波 ByteWave 360导航 360安全浏览器
0 条评论 0 人参与