Go语言之goroutine和通道
goroutine
在Go里,每一个并发执行的活动称为goroutine
。 如果你是一名Java程序员,可以把goroutine
比作为线程,但是goroutine
和线程在数量上有很大的差别,原因在于Go语言引入了协程的概念,协程相比于线程是一种用户态的线程,协程更加轻量,实用更加经济,因此同样的服务器可以开销的协程数量要比线程多很多。
goroutine和协程的区别:
- goroutine是协程的go语言实现,相当于把别的语言的类库的功能内置到语言里。从调度上看,goroutine的调度开销远远小于线程调度开销。
- 不同的是:Golang在runtime,系统调用等多方面对goroutine调度进行了封装和处理,即goroutine不完全是用户控制,一定程度上由go运行时(runtime)管理,好处:当某goroutine阻塞时,会让出CPU给其他goroutine。
线程和goroutine的区别:
- OS的线程由OS内核调度,每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用一个调度器内核函数。这个函数暂停当前正在运行的线程,把他的寄存器信息保存到内存中,查看线程列表并决定接下来运行哪一个线程,再从内存中恢复线程的注册表信息,最后继续执行选中的线程。这种线程切换需要一个完整的上下文切换:即保存一个线程的状态到内存,再恢复另外一个线程的状态,最后更新调度器的数据结构。某种意义上,这种操作还是很慢的。
- 从调度上讲,线程的调度由 OS 的内核完成;线程的切换需要CPU寄存器和内存的数据交换,在线程切换的过程中需要保存/恢复所有的寄存器信息,比如16个通用寄存器,PC(Program Counter),SP(Stack Pointer),段寄存器等等,从而切换不同的线程上下文。 其触发方式为 CPU时钟。而goroutine 的调度 则比较轻量级,由go自身的调度器完成;Go运行的时候包涵一个自己的调度器,这个调度器使用一个称为一个M:N调度技术,m个goroutine到n个os线程(可以用GOMAXPROCS来控制n的数量),Go的调度器不是由硬件时钟来定期触发的,而是由特定的go语言结构来触发的,他不需要切换到内核语境,所以调度一个goroutine比调度一个线程的成本低很多。其只关心当前go程序内协程的调度;触发方式为 go内部的事件,如文件和网络操作垃圾回收,time.sleep,通道阻塞,互斥量操作等。在同一个原生线程里,若当前goroutine不发生阻塞,那么不会主动让出CPU给其他同一线程的goroutine的。在go程序启动时,会首先创建一个特殊的内核线程sysmom,负责监控和调度。
- 从栈空间上,goroutine的栈空间更加动态灵活。每个OS的线程都有一个固定大小的栈内存,通常是2MB,栈内存用于保存在其他函数调用期间哪些正在执行或者临时暂停的函数的局部变量。这个固定的栈大小,如果对于goroutine来说,可能是一种巨大的浪费。作为对比goroutine在生命周期开始只有一个很小的栈,典型情况是2KB, 在go程序中,一次创建十万左右的goroutine也不罕见(2KB*100,000=200MB)。而且goroutine的栈不是固定大小,它可以按需增大和缩小,最大限制可以到1GB。
- goroutine没有一个特定的标识。在大部分支持多线程的操作系统和编程语言中,线程有一个独特的标识,通常是一个整数或者指针,这个特性可以让我们构建一个线程的局部存储,本质是一个全局的map,以线程的标识作为键,这样每个线程可以独立使用这个map存储和获取值,不受其他线程干扰。goroutine中没有可供程序员访问的标识,原因是一种纯函数的理念,不希望滥用线程局部存储导致一个不健康的超距作用,即函数的行为不仅取决于它的参数,还取决于运行它的线程标识。
简单的示例代码如下:在Go里,每一个并发执行的活动称为goroutine
。
简单的示例代码如下:
f() //调用f();等它返回
go f() // 新建一个调用分()的goroutine,不用等待
在下面的例子中,主goroutine
计算第45个斐波那契数。因为它使用非常低效的递归算法,因此需要大量的时间来执行,在此期间我们提供一个可见的提示,显示一个字符串”spinner“来指示程序依然在运行。
package main
import (
"fmt"
"time"
)
func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}
func fib(x int) int {
if x < 2 {
return x
}
return fib(x-1) + fib(x-2)
}
func main() {
go spinner(100 * time.Microsecond)
const n = 45
fibN := fib(n) // slow
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}
若干秒后,fib(45)返回结果,如下图所示:
然后main
函数返回,所有的goroutine
都暴力地直接终结,然后程序退出。
通道
如果说goroutine
是Go程序并发的执行体,通道就是它们之间的连接。通道是可以让一个goroutine
发送特定值到另一个goroutine
的通信机制。每一个通道是一个具体类型的导管,叫做通道的元素类型。一个有int
类型元素的通道写为chan int
。
使用内置的make
函数来创建一个通道:
ch := make(chan int) // ch的类型是 chan int
像map
一样,通道是一个使用make
创建的数据结构的引用。当复制或者作为参数传递到一个函数时,复制的是引用,这样调用者和被调用者都引用同一份数据结构。和其他引用类型一样,通道的零值是nil
。
通道有两个主要的操作:发送和接收,这两者统称为通信。send
语句从一个goroutine
传输一个值到另一个在执行接收表达式的goroutine
。两个操作都使用<-
操作符书写。发送语句中,通道和值分别在<-
的左右两边。在接收表达式中,<-
放在通道操作数的前面。
具体书写格式如下:
ch <- x //发送语句
x = <- ch // 赋值语句中的接收表达式
<- ch // 接收语句,丢弃结果
通道支持第三个操作:关闭,它设置一个标志位来指示值当前已经发送完毕,这个通道后面没有值了,关闭后的发送操作将导致宕机。在一个已经关闭的通道上进行接收操作,将获取所有已经发送的值,直到通道为空,这是任何接收操作会立即完成,同时获取到一个通道元素类型对应的零值。
调用内置的close
函数来关闭通道:
close(ch)
无缓冲通道
使用简单的make
调用创建的通道叫做无缓冲通道,但make
还可以接受第二个可选参数,一个表示通道容量的整数。如果容量是0,make
创建一个无缓冲的通道。
ch = make(chan int) // 无缓冲通道
ch = make(chan int, 0) // 无缓冲通道
ch = make(chan int, 3) // 容量为3的缓冲通道
无缓冲通道上的发送操作将会阻塞,直到另一个goroutine
在对应的通道上执行接收操作,这时值传送完成,两个goroutine
都可以继续执行。相反,如果接收操作先执行,接收方goroutine
将阻塞,直到另一个goroutine
在同一个通道发送一个值。
使用无缓冲通道进行通信导致发送和接收goroutine
同步化。因此,无缓冲通道也称为同步通道。当一个值在无缓冲通道上传递时,接收值后发送方goroutine
才被再次唤醒。
package main
import "fmt"
func main(){
ch:=make(chan int) //这里就是创建了一个channel,这是无缓冲管道注意
go func(){ //创建子go程
for i:=0;i<=6;i++{
ch<-i //循环写入管道
fmt.Println("写入",i)
}
}()
for i:=0;i<6;i++{ //主go程
num:=<-ch //循环读出管道
fmt.Println("读出",num)
}
}
缓冲通道
缓冲通道有一个元素队列,队列的最大长度在创建的时候通过make
的容量参数来设置。如下代码创建了一个带有10个字符串的缓冲通道:
ch = make(chan string,10)
缓冲通道上的发送操作在对列的尾部插入一个元素,接收操作从队列的头部移除一个元素。如果通道满了,发送操作会阻塞所在的goroutine
直到另一个goroutine
对它进行接收操作来腾出可用的空间。反过来,如果通道是空的,执行接收操作的goroutine
阻塞,直到另一个goroutine
在通道上发送数据。
现在,我们可以在通道上无阻塞的发送三个值,但是在发送第四个值的时候就会阻塞。
package main
func main() {
ch := make(chan string, 3)
ch <- "A"
ch <- "B"
ch <- "C"
ch <- "D"
}
在我们向管道塞入第四个值的时候,程序爆出了死锁的异常,如下图:
但是当我们在执行第四次向通道塞值的时候,从通道取出一个值,就可以安全的进行第四次塞值了,并且成功的打印出了队列的第一个元素A,如下图:
管道
通道可以用来连接goroutine
,这样一个具体的输出是另一个的输入。管道一般是由三个goroutine
组成,使用两个通道连接起来。
如下代码所示:
package main
import "fmt"
func main() {
naturals := make(chan int)
squares := make(chan int)
// counter
go func() {
for x := 0; x< 100; x++ {
naturals <- x
}
close(naturals)
}()
// squares
go func() {
for {
x,ok := <-naturals
if !ok {
break // 通道关闭并且读完
}
squares <- x * x
}
close(squares)
}()
// printer(在主goroutine中)
for x := range squares{
fmt.Println(x)
}
}
结束时,关闭每一个通道不是必需的,只有在通知接收方goroutine
所有数据都发送完毕的时候才需要关闭通道。通道也是可以通过垃圾回收器根据它是否可以访问来决定是否回收它,而不是根据它是否关闭。
不要将这个close
操作和对于文件的close
操作混淆。当结束的时候对每一个文件调用Close
方法是非常重要的。
单向通道
Go也提供了单向通道类型,仅仅导出发送或者接收操作。类型chan <- int
是一个只能发送的通道,允许接收但是不能发送。(<-
操作符相对于chan
关键字的位置是一个帮助记忆的点)。
package main
import "fmt"
// 单向输出通道 chan<-
func counter(out chan<- int) {
for x := 0; x < 100; x++ {
out <- x
}
}
// 单向输出通道 chan<-
func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
}
// 单向输入通道 <-chan
func printer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}
并行循环
为了演示并行循环,我们考虑生成一批全尺寸图像的缩略图,代码如下:
package main
import (
"gopl.io/ch8/thumbnail"
"io/ioutil"
"log"
)
func makeThumbnails(filenames []string) {
for _, f := range filenames {
if _, err := thumbnail.ImageFile(f); err != nil {
log.Println(err)
}
}
}
func main() {
rootPath := "W:\\Google_Download\\壁纸\\动漫壁纸\\"
//var rootPath string = "W:\\Google_Download\\壁纸\\动漫壁纸\\"
files, _ := ioutil.ReadDir(rootPath)
var fileNames []string
/*
第一种读取文件列表的方法
*/
for _, f := range files {
//fmt.Println(f.Name())
fileNames = append(fileNames, rootPath+f.Name())
}
makeThumbnails(fileNames)
}
其中需要导入的gopl.io/ch8/thumbnail
包,从下面的网站下载:
GitHub - adonovan/gopl.io: Example programs from "The Go Programming Language"](https://github.com/adonovan/gopl.io/)
生成后的结果如图所示,都是我喜欢的动漫图片,如果也喜欢,私信我给你发哈~
以上就是Go语言关于goroutine
和通道的内容,关于goroutine
和通道其实还有很多可以深挖的东西,我们后面会继续学习。希望这篇文章可以帮助到你~