Go 是 Google 公司推出的新的一门编译型语言,Go 的出现能充分利用计算机的多核多 cpu 的硬件优势,它吸收了很多编程语言的优点,有着 C 语言强大的的执行速度,也有着 Python 快速开发的能力。特别是语言层面就支持并发,并发实现起来也其他语言要简单很多,另外 Go 在区块链,云计算场景应用比较广泛。
Go 语言很大的一个特点就是从语言层面支持并发,并且加入了 channel (管道)通信机制,通过 channel 可以实现不同的 goroutine 之间的相互通信。因为后面大多数的案例都会用到 goroutine 和 channel ,所以最开始就先为大家介绍它们两个的相关知识。
在讲 goroutine , channel 之前,我们先来说一下进程 & 线程 & 协程的之间的关系。
进程 & 线程 & 协程
进程是程序在执行过程中分配和管理资源的基本单位,是竞争计算机系统资源的基本单位。但是创建进程耗费的资源比较大,计算机能运行的进程数量也是有限的,所以大家就会去使用线程代替进程处理一些并发的任务。
线程是进程的一个实例,一个进程可以包含多个线程,多线程要多进程运行效率更快,但是线程之间的切换是比较耗费资源的。
协程呢它跟线程差不多,它是更加轻量级的线程,比线程最大的优势就是协程切换没有线程切换那么大的开销,编译器内部会做优化。在程序中线程使用数量的越多,用协程能发挥的优势也就越大。
给大家分享个区分进程与线程/协程的例子。比如常见的迅雷软件,启动迅雷这个应用可以理解成一个计算机启动了一个进程,迅雷同时下载多个资源可以理解成多线程或者多协程在下载。
goroutine如何使用?
协程(goroutine)是Go语言中的轻量级线程实现,在一个函数调用前加上 go 关键字就会启动一个新的goroutine去并发执行。我们先来写一个简单使用 goroutine 的例子。
package main
import (
"fmt"
"time"
)
func Print() {
fmt.Println("hello world")
}
func main() {
go Print()
//保证主进程等待协程全部执行结束
time.Sleep(time.Second * 1)
}
运行代码你会发现什么都没有输出,是不是很诧异。明明启动了一个 goroutine 来打印“hello word”,结果什么输出都没有。其原因就在于协程 Print 函数还没有执行的时候,主线程就已经运行结束了。
那该如何解决呢?聪明的同学可以想到使用 Sleep 函数让主线程阻塞一会等待Print函数执行完。下面来修改代码。
package main
import (
"fmt"
"time"
)
func Print() {
fmt.Println("hello world")
}
func main() {
go Print()
//保证主进程等待协程全部执行结束
time.Sleep(time.Second * 1)
}
执行代码,顺利的打印出了"hello world",但采用这种方法解决并不是很好。因为主线程在等待 goroutine 执行完的耗时大家是不太确定的。
我们再来演示另一种情况,多个 goroutine 同时操作同一个数据又会发生什么问题?
package main
import (
"time"
)
var (
currentMap = make(map[int]int, 30)
)
func setMap(n int) {
currentMap[n] = n
}
func main() {
// 启动30个goroutine,往map放值
for i := 1; i <= 30; i++ {
go setMap(i)
}
// 保证主进程等待协程全部执行结束
time.Sleep(time.Second * 3)
}
在控制台运行 `go run map.go`,大家控制台可能会提示一个错误 `deadlock!` 死锁。
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
针对以上遇到的两个问题,那有没有一个更好的机制去解决呢?当然有,那就是 channel 。下面我们来详细介绍 channel 。
什么是channel?
channel 可以理解成管道,是一种数据结构(先进先出的队列),channel 里面可以存放的不同类型的数据。channel 主要用来作为多个 goroutine 之间数据的传递,并且它是线程安全的。所以就解决了多个 goroutine 操作同一个数据容易造成死锁的问题,后面的案例会给大家演示。
`非缓冲的channel`它有这么一种机制:如果想要获取 channel 的数据,那就必须事先存入数据到 channel 中,否则主线程会一直阻塞。通过这种机制就可以避免使用 `time.Sleep` 方式休眠主进程。下面我们来看它如何使用。
package main
import (
"fmt"
)
func Print(channel chan string) {
fmt.Println("hello world")
// 往channel中存入数据
channel <- "ok"
}
func main() {
// 定义一个channel
channel := make(chan string)
go Print(channel)
// 等待channel中存入数据,否则主线程一直阻塞
<- channel
}
上面的案例使用的 `非缓冲channel` 去演示的,有些同学会有疑问既然有了非缓冲那肯定也有缓冲的 channel 。它们两个有什么区别呢?
非缓冲和缓冲 channel 区别
非缓冲的 channel 不能缓存数据,如果往 channel 放入一个数据,再放入下一个数据之前,那就必须把之前存进去的先取出来,否则就会造成死锁。缓冲的 channel 则不同,它可以指定缓存的数量。下面来举个例子。
package main
import (
)
func main() {
// 非缓冲的channel
channel := make(chan string)
channel <- "ok"
//不获取channel中的数据
}
执行这段代码控制台会显示一下的错误
fatal error: all goroutines are asleep - deadlock!
然后我们把上面的代码给改成缓冲的 channel ,其实就是把 channel 定义换成`channel := make(chan string,1)`。这里的1就指定是 channel 的缓冲容量。这样修改的话代码就不会造成死锁了。
但是这里特别需要注意:定义缓冲的 channel 当容量满的情况下,如果不取出里面的数据就往里面存放的话同样也会造成死锁。
比如如下代码::
package main
import (
)
func main() {
// 缓冲的channel
channel := make(chan string,1)
channel <- "ok"
channel <- "ok2"
}
channel关闭和遍历
channel 关闭可以使用系统 close 方法,关闭的 channel 是只读的状态,只能读取,不能添加。我们来看代码:
package main
import (
)
func main() {
channel := make(chan string,3)
channel <- "ok"
channel <- "ok2"
// 关闭channel
close(channel)
// 如果再向关闭的channel增加的数据的话,会报错。
channel <- "ok3"
}
channel 遍历很简单,使用 `for range` 即可。但是特别需要注意,在主线程中使用 `for range` 遍历的 channel 如果不是 close 的,那则必须在循环中加条件判断 channel 长度,如果长度为 0 进而 break 掉,否则程序也会造成死锁。
package main
import (
"fmt"
)
func main() {
channel := make(chan string)
channel <- "ok"
channel <- "ok2"
// 关闭channel
close(channel)
for data := range channel {
fmt.Println(data)
if len(channel) <= 0 {
break
}
}
}
监听多个channel
go 可以使用 `select` 语法同时监听多个 channel 的数据流动。使用的方式也比较简单,这个跟其他语言中的 `switch` 语法比较相似,相信大家看了下面的代码就能明白,这里就不做过多解释。
package main
import (
"fmt"
)
func main() {
channel := make(chan int,5)
channel2 := make(chan int,5)
for i := 0; i < 5; i++ {
channel <- i
}
for i := 0; i < 5; i++ {
channel2 <- i
}
for {
select {
case num := <-channel:
fmt.Println("num = ", num)
case num := <-channel2:
fmt.Println("num = ", num)
default :
fmt.Printf("什么都取不到了,程序退出。\n")
return
}
}
}
运行结果如下:
num = 0
num = 0
num = 1
num = 2
num = 1
num = 3
num = 2
num = 4
num = 3
num = 4
什么都取不到了,程序退出。