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

什么是信道

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

声明信道

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

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

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

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

 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)
}
}

因为信道的 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( * time.Second)
fmt.Println("main function")
}

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

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

 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,然后将它作为参数传递给 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 协程中加入一个休眠,来更好的理解阻塞的概念。

 package main

 import (
"fmt"
"time"
) func hello(done chan bool) {
fmt.Println("hello go routine is going to sleep")
time.Sleep( * 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")
}

在上面程序中的第 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

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

 package main

 import (
"fmt"
) func calcSquares(number int, squareop chan int) {
sum :=
for number != {
digit := number %
sum += digit * digit
number /=
}
squareop <- sum
} func calcCubes(number int, cubeop chan int) {
sum :=
for number != {
digit := number %
sum += digit * digit * digit
number /=
}
cubeop <- sum
} func main() {
number :=
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 计算 number 每一位的平方和,并将结果发送给信道 squareop。同样地,在第 17 行,函数calcCubes 计算 number 每一位的立方和,并将结果发送给信道 cubeop

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

Final output   

死锁

使用信道是要考虑的一个重要因素是死锁(Deadlock)。如果一个协程发送数据给一个信道,而没有其他的协程从该信道中接收数据,那么程序在运行时会遇到死锁,并触发 panic 。

同样地,如果一个协程从一个信道中接收数据,而没有其他的协程向这个信道发送数据,那么程序同样造成死锁,触发 panic 。

package main

func main() {
ch := make(chan int)
ch <-
}

上面的程序中,创建了一个信道 ch,并通过 ch <- 5 向其中写入 5 。这个程序中没有其他协程从 ch 中接收数据,因此程序在运行时触发 panic,错误如下:

fatal error: all goroutines are asleep - deadlock!

goroutine  [chan send]:
main.main()
/tmp/sandbox249677995/main.go: +0x80

单向信道

目前我们讨论的信道都是双向信道,数据既可以发送到双向信道,也可以从双向信道中读取。同样也可以创建单向信道,即只能发送数据或只能接收数据的信道。

package main

import "fmt"

func sendData(sendch chan<- int) {
sendch <-
} func main() {
sendch := make(chan<- int)
go sendData(sendch)
fmt.Println(<-sendch)
}

在上面程序中的第 10 行,我们创建了一个只写(send only)信道 sendch 。chan<- int 表示只能发送数据,因为箭头的方向指向 chan。在第 12 行,我们试图从一个只写信道中接收数据,这是非法的,程序将无法通过编译,编译器报错如下:

main.go:: invalid operation: <-sendch (receive from send-only type chan<- int)

一切都很好,但是如果无法读取,创建一个只写通道有什么用呢?

这就是信道转型的用途。可以将双向信道转换为只写或只读信道,但是反过来却不行。

package main

import "fmt"

func sendData(sendch chan<- int) {
sendch <-
} func main() {
chnl := make(chan int)
go sendData(chnl)
fmt.Println(<-chnl)
}

在上面程序中的第 10 行,创建了一个双向信道 chnl。在第 11 行它被作为参数传递给协程 sendData。第 5 行,sendData 函数通过形参 sendch chan<- int 将该信道转换成只写信道。因此在 sendData 中该信道为只写信道,而在主协程中该信道为双向信道。程序将打印:10

关闭信道以及使用 range for 遍历信道

发送者可以关闭信道以通知接收者将不会再发送数据给信道。

在从信道接收数据时,接收者可以通过一个额外的变量来检测信道是否已经被关闭。

v, ok := <- ch

上面的语句中 ok 返回 true 表示成功的接收到了发送的数据,如果 ok 返回 false 则表示信道已经被关闭。从已关闭的信道中读取到的数据为信道类型的 0 值。比如从一个被关闭的 int 信道中读取数据,那么将得到 0 。

package main

import (
"fmt"
) func producer(chnl chan int) {
for i := ; i < ; 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 向信道 chnl 中写入 0 到 9 并关闭该信道。主协程在第 16 行进行无限 for 循环,并在循环中检测 ok 的值判断信道是否已经被关闭(第 18 行)。如果 ok 是 false 表示信道已经被关闭,则通过 break 退出循环。否则接收数据并打印 ok 的值。程序的输出为:

Received   true
Received true
Received true
Received true
Received true
Received true
Received true
Received true
Received true
Received true

range for 可以用来接收一个信道中的数据,直到该信道关闭。

让我们用 range for 重写上面的程序:

 package main

 import (
"fmt"
) func producer(chnl chan int) {
for i := ; i < ; i++ {
chnl <- i
}
close(chnl)
}
func main() {
ch := make(chan int)
go producer(ch)
for v := range ch {
fmt.Println("Received ",v)
}
}

在第6 行,for range 不断从信道 ch 中接收数据,直到该信道关闭。一旦 ch 关闭,循环自动退出。程序的输出如下

Received
Received
Received
Received
Received
Received
Received
Received
Received
Received

如果仔细观察该程序,你可以注意到,在 calcSquares 和 calcCubes 函数中查找一个数的每一位的代码重复了。我们将这段代码提取到一个单独的函数,并异步调用它。

 package main

 import (
"fmt"
) func digits(number int, dchnl chan int) {
for number != {
digit := number %
dchnl <- digit
number /=
}
close(dchnl)
}
func calcSquares(number int, squareop chan int) {
sum :=
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 :=
dch := make(chan int)
go digits(number, dch)
for digit := range dch {
sum += digit * digit * digit
}
cubeop <- sum
} func main() {
number :=
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 中异步调用。在第 13 行,当数字中没有更多的位数时,信道被关闭。calcSquares 和 calcCubes 分别在各自的信道上使用 range for 直到信道被关闭。程序中的其他部分都是一样的。程序的输出依然是:

Final output   

这就来到了本教程的最后。信道中还有更多的概念,比如缓冲信道,工作池和 select 。

本文转自:https://blog.csdn.net/u011304970/article/details/76168257

Golang教程:goroutine信道的更多相关文章

  1. golang的goroutine与channel

    Golang的goroutine是非抢占式的, 令人相当蛋疼! 有痛不能呻吟...只能配合channel在各goroutine之间传递信号来实现抢占式, 而这形成了golang最灵活与最具性能的核心. ...

  2. 谈谈Golang中goroutine的调度问题

    goroutine的调度问题,同样也是我之前面试的问题,不过这个问题我当时并不是很清楚,回来以后立马查阅资料,现整理出来备忘. 有一些预备知识需要说明,就是操作系统中的线程.操作系统中的线程分为两种: ...

  3. [转]golang的goroutine调度机制

    golang的goroutine调度机制 版权声明:本文为博主原创文章,未经博主允许不得转载.   目录(?)[-] 一直对goroutine的调度机制很好奇最近在看雨痕的golang源码分析基于go ...

  4. Golang教程:goroutine协程

    在上一篇中,我们讨论了并发,以及并发和并行的区别.在这篇教程中我们将讨论在Go中如何通过Go协程实现并发. 什么是协程 Go协程(Goroutine)是与其他函数或方法同时运行的函数或方法.可以认为G ...

  5. Golang的goroutine协程和channel通道

    一:简介 因为并发程序要考虑很多的细节,以保证对共享变量的正确访问,使得并发编程在很多情况下变得很复杂.但是Go语言在开发并发时,是比较简洁的.它通过channel来传递数据.数据竞争这个问题在gol ...

  6. Golang控制goroutine的启动与关闭

    最近在用golang做项目的时候,使用到了goroutine.在golang中启动协程非常方便,只需要加一个go关键字: go myfunc(){ //do something }() 但是对于一些长 ...

  7. [golang学习] goroutine调度

    这两天有些闲功夫, 学习下golang, 确实非常简洁. 不过有些缺憾. 在我的测试中. golang的调度(goroutine)似乎不是非常好. func say(k int) { fmt.Prin ...

  8. golang核心Goroutine和channel

    一.Goroutine 1.介绍 goroutine简介 goroutine是go语言中最为NB的设计,也是其魅力所在,goroutine的本质是协程,是实现并行计算的核心.goroutine使用方式 ...

  9. Golang教程:变量

    声明单一变量 声明一个变量的语法为:var name type,例如 package main import "fmt" func main() { var age int // ...

随机推荐

  1. Replication--修改复制代理配置来查看代理运行情况

    1>在复制监视器中选中订阅右键 2>选择代理配置文件 3>将代理配置文件设置为”详细历史记录代理配置文件“,确定以保存 4>重启代理 5>代理运行一段时间后,重启代理 6 ...

  2. c#设计模式系列:模板方法模式(Template Method Pattern)

    引言 提到模板,大家肯定不免想到生活中的"简历模板"."论文模板"."Word中模版文件"等,在现实生活中,模板的概念就是--有一个规定的格 ...

  3. mysql --initialize specified but the data directory has files in it

    删除 *.ini 文件中的datadir=“....”目录下的文件,即可.

  4. CharSequence与String的区别

    CharSequence与String都能用于定义字符串,但CharSequence的值是可读可写序列,而String的值是只读序列. 原文: http://blog.csdn.net/joy_zha ...

  5. [Swift]多维数组的表示和存储:N维数组映射到一维数组(一一对应)!

    数组:有序的元素序列. 若将有限个类型相同的变量的集合命名,那么这个名称为数组名.组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量.用于区分数组的各个元素的数字编号称为下标.数组 ...

  6. 如何在Linux下禁用IPv6

    如何在Linux下禁用IPv6 echo 1 > /proc/sys/net/ipv6/conf/all/disable_ipv6                            禁用IP ...

  7. 关于Socket通讯中的Close_wait状态

    关于Socket通讯中的Close_wait状态 文/转 编辑 编者按:使用Socket通讯,有时我们查看端口状态的时候,经常会发现Socket处于close_wait状态,从而影响系统性能,此文或许 ...

  8. python3入门之类

    在面向对象的语言中,类是最重要的一环,python自然拥有类这个机制.python的类机制,与C++,java的区别不是很大,类的大多数的重要特性都被沿用了,一样可以多态,抽象,封装: python3 ...

  9. 10分钟教你用Python玩转微信之好友性别比例统计分析

    01 前言+效果展示 想必,微信对于大家来说,是再熟悉不过的了.那么,大家想不想探索一下微信上的各种奥秘呢?今天,我们一起来简单分析一下微信上的好友性别比例吧~废话不多说,开始干活. 结果如下: 02 ...

  10. NVIDIA Jetson TX2刷机

    官方安装教程 JetPack下载 主机端环境准备 需要在PC端安装虚拟机,虚拟机中安装Ubuntu14.04系统. 按照上面的地址下载JetPack-L4T-3.1-linux-x64.run 主机端 ...