文档翻译自Golang channels tutorialhtml
Golag内置并发功能的特性。经过把标识符go放置在将要执行的函数前,被执行的代码能够在相同的地址空间内启动独立的并发线程。在Golang中,称为goroutine。这里我想特别强调下并发并不意味着并行。Goroutines是建立并发体系结构的手段,能够在硬件容许的状况下并行执行。并发并不意味着并行golang
咱们先试试goroutine的示例代码:数组
func main() {
// Start a goroutine and execute println concurrently
go println("goroutine message")
println("main function message")
}
复制代码
这段程序将会打印main function message,可是可能打印goroutine function message。在main方法中衍生goroutine协程,可是在main方法中并无等待goroutine方法执行完,此时程序就只会打印main function message,反之会打印出goroutine message。缓存
你想必会想golang必定会有解决这种不肯定性的方案,这就是我将要介绍的关于Golang通道的知识点。bash
通道能够将并发执行的程序同步,并经过特定值类型实现并发进程相互通讯。通道有如下几个组成:经过通道传递的数据类型、通道的缓存容量、标识数据方向的标识符<-。开发者能够经过内置函数make为通道分配内存。并发
i := make(chan int)
s := make(chan string, 3)
r := make(<-chan bool) // can only read from
w := make(chan<- []os.FileInfo) // can only write to
复制代码
通道是基础的变量,使用场景与其它基础类型同样。诸如:结构元素、函数变量、函数返回值、甚至能够做为其它通道的类型:异步
c := make(chan <- chan bool)
func readFromChannel(input <-chan string)
func getChannel() chan bool {
b := make(chan bool)
return b
}
复制代码
能够经过标识符<-向通道中读写数据,如何建立通道并对它作些基础操做,如今让咱们回到本文的第一个例子程序中,探究通道能够帮助咱们事先什么功能?函数
package main
import (
"fmt"
)
func main() {
done := make(chan bool)
go func() {
fmt.Println("Goroutine message")
done <- true
}()
fmt.Println("Main message")
<-done
}
复制代码
这段程序将会将程序中全部message信息都打印出来,并不存在其它可能性。为何会这样?done通道没有任何缓冲(因为咱们没有为通道定义容量)。全部没有缓冲的通道都将会阻塞程序的执行,除非通道的发送和接受数据的功能都已经为通讯作好准备,这也是无缓冲通道也被称为同步的缘由。在咱们的示例代码main函数中,消费done通道的数据一直会阻塞main函数的执行,直至goroutine向main通道中写入数据。所以程序只有在成功读取done通道的数据后才会结束。ui
对于有缓冲的通道:当缓存不为空时,全部经过通道读数据的操做都不会阻塞程序的执行。当缓存没有满时,全部向通道中写数据的操做都不会阻塞程序的执行。这样的通道能够称为异步。下面的代码将展现同步通道与异步通道的差别:this
package main
import (
"fmt"
"time"
)
func main() {
message := make(chan string)
count := 3
go func () {
for i := 1; i <= count; i ++ {
fmt.Println("send message")
message <- fmt.Sprintf("message %d", i)
}
}()
time.Sleep(time.Second * 3)
for i := 1; i <= count; i ++ {
fmt.Println(<-message)
}
}
复制代码
上面的例子中message通道是同步的,程序的输出以下:
send message
// wait for 3 seconds
message 1
send message
send message
message 2
message 3
复制代码
如你所见,当第一次向通道中写入数据后,其它的写入数据操做都将被阻塞,直到3秒后,通道中的数据被消费。
若是咱们使用的是有缓存的通道, 例如向下面那样建立message通道: message := make(chan tring, 2)
这时,程序将是下面的输出:
send message
send message
send message
// wait for 3 seconds
message 1
message 2
message 3
复制代码
经过上面的输出咱们能够看到,全部缓存通道的写操做并无由于通道中的数据没有被消费而阻塞。经过更改通道的容量,开发者能够控制系统吞吐量。
如今咱们先看下面这段向通道中读写数据的示例代码片断:
package main
import (
"fmt"
)
func main() {
c := make(chan int)
c <- 42
val := <-c
fmt.Println(val)
}
复制代码
执行上面的代码,将会获得下面的输出结果:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/fullpathtofile/channelsio.go:5 +0x54
exit status 2
复制代码
上面的输出结果是典型的死锁信息。这是因为两个goroutines相互等待对方,以致于任何一方都不能完成执行操做。Golang能够在运行时发现错误,而后将错误信息打印出来。所以这个错误是因为通道的通讯被阻塞所致使的。
代码能够在单线程中按照顺序一行行的执行,只有当接收方顺利读取通道中的数据后,(c <- 42)才能够以同步的方式向通道中写入数据。所以开发者要在向通道写数据的代码后,添加向通道中消费数据的功能代码。
为了使程序顺利运行,开发者能够参考下面的修改:
package main
import (
"fmt"
)
func main() {
c := make(chan int)
go func() {
c <- 42
}()
val := <-c
fmt.Println(val)
}
复制代码
在上面的了例子中,实现屡次经过通道读写数据的代码以下:
for i := 1; i <= count; i ++ {
fmt.Println(<-message)
}
复制代码
因为通道被消费的数据个数不能多于写入通道的数据个数,为了顺利经过通道读数据并且不产生死锁,开发者必须准确知道已经向通道中写入多少条数据。
在Golang中,经过range表达式的语法能够实现对数组、字符串、切片、maps和通道的遍历。对于通道而言,在通道关闭时会触发遍历通道的代码。能够参照下面的代码(下面的代码并不能正常工做):
package main
import (
"fmt"
)
func main() {
message := make(chan string)
count := 3
go func() {
for i := 1; i <= count; i ++ {
messager <- fmt.Sptintf("message %d", i)
}
}()
for msg := range message {
fmt.Println(msg)
}
}
复制代码
很是抱歉上面的程序并不能正常工做。我在上文也已经强调了,当通道被关闭时,range表达式才会被触发。所以为了程序可以正常工做,开发者必须在程序中调用close函数。这样gotroutine的代码将会是下面的样子:
go func() {
for i := 1; i <= count; i ++ {
message <- fmt.Sprintf("message %d", i)
}
close(message)
}()
复制代码
关闭通道的函数还具备一个额外的特性 - 当调用关闭通道的函数后,而后向空通道中读取数据或是在同一个线程中读数据,此时并不会阻塞程序。
package main
import (
"fmt"
)
func main() {
done := make(chan bool)
close(done)
fmt.Println(<- done) // false
fmt.Println(<- done) // false
}
复制代码
这个特性也可使用在同步化goroutines的代码中。如今让咱们在回顾下同步goroutine的代码:
func main() {
done := make(chan bool)
go func() {
println("goroutine message")
// We are only interested in the fact of sending itself,
// but not in data being sent.
done <- true
}()
println("main function message")
<-done
}
复制代码
上面done通道只是为了使得程序能够同步执行,并不须要经过通道传递数据。所以咱们能够将上面的代码做以下修改:
func main() {
// Data is irrelevant
done := make(chan struct{})
go func() {
println("goroutine message")
// Just send a signal "I'm done"
close(done)
}()
println("main function message")
<-done
}
复制代码
当开发者关闭了goroutine的通道后,并不会阻塞向通道读数据的操做,所以main函数能够继续执行。
在真实的开发需求中,开发者每每须要面对多个goroutine和通道。独立运行的模块越多,越是须要高效率的同步。让咱们来看一个更加复杂的例子:
func getMessagesChannel(msg string, delay time.Duration) <-chan string {
c := make(chan string)
go func() {
for i := 1; i <= 3; i++ {
c <- fmt.Sprintf("%s %d", msg, i)
// Wait before sending next message
time.Sleep(time.Millisecond * delay)
}
}()
return c
}
func main() {
c1 := getMessagesChannel("first", 300)
c2 := getMessagesChannel("second", 150)
c3 := getMessagesChannel("third", 10)
for i := 1; i <= 3; i++ {
println(<-c1)
println(<-c2)
println(<-c3)
}
}
复制代码
上面的代码中,经过建立通道、衍生spawns的goroutine函数和休眠固定的时间后返回通道。咱们能够明显发现c3通道的时间间隔是最短的,所以指望先打印出c3通道中消息。然而程序的输出倒是下面的内容:
first 1
second 1
third 1
first 2
second 2
third 2
first 3
second 3
third 3
复制代码
显然易见,程序的输出是正确的。按顺序的调用getMessagesChannel函数会衍生出相应goroutine和新的无缓存通道,所以只有当无缓存通道中数据被消费后,相应goroutine的代码才会继续向通道中写入数据。若是咱们指望程序的运行结果是那个goroutine更快的向通道写入数据,将会被优先消费。
在Golang中,select关键词能够用来在通讯中处理多通道的消息传递。这很像其它语言的switch语法,可是select使用场景只能用来向通道中读写数据。所以为了高效的处理多通道问题,能够参照下面的代码:
for i := 1; i <= 9; i++ {
select {
case msg := <-c1:
println(msg)
case msg := <-c2:
println(msg)
case msg := <-c3:
println(msg)
}
}
复制代码
请注意数字9:因为每一个通道都会3次向通道中写入数据,所以须要9次循环使用select。 如今咱们将会获得指望的输出结果,不一样的读写操做间也不会相互阻塞。输出是:
first 1
second 1
third 1 // this channel does not wait for others
third 2
third 3
second 2
first 2
second 3
first 3
复制代码
通道是Golang中很是强大也颇有趣的功能。可是若是想高效的使用这个语法特性,必须理解它的实现原理。在这篇文章中我试图向读者介绍关于使用通道的最基本知识点。若是你想进行深刻的研究,能够参考下面的链接: