本文首发于 PRSDigg(顶呱呱) © 2022 阿坦
转载请注明出处
原创不易,请多转发点赞
众所周知 Go 语言是高并发开发利器,它可以使多核(多线程)CPU 的性能得到充分利用。
那么,Go 具体是怎么实现并发的呢?这篇教程试图用最简洁的文字和代码给你讲清楚。
goroutine
首先,需要引入一个概念:goroutine
.
线程这个概念,可能知道的人会更多一些。我想借助这个概念来帮助你搞明白 goroutine
.
当我们在进行一项任务,比如打开一个 word 文档,可以同时在它里面进行打字、拼写检查、打印等。同时干这几件事,就是在同时运行多项子任务。这每一个子任务——打字、拼字检查——它们就分别是一个线程。
在 Go 语言里面,也类似,在执行一段 Go 语言代码的时候,可以同时开启几项不同的工作,让它们交替进行(并发)或者同时进行(并行)。每一个并发(并行)的工作,就是一个 goroutine
. 我听到有些老师把它叫做 Go 程,我觉得很贴切易懂。所以我想,不如接下来我们也把它叫做「Go 程」吧。
那么,具体在代码里是怎么实现的呢?
代码实现
在 Go 语言里,一段代码一般至少要有 3 个部件:包名、引入的包、以及 main()
函数。
我们先来看一个简单的 Go 代码:
package main
import "fmt"
func main() {
fmt.Println("Hello")
fmt.Println("World")
fmt.Println("Say something.")
}
整个 main()
函数的任务是输出三个字符串 "Hello", "World", "Say something". 来看看运行的结果:
现在,假设我们想把输出 "Hello" 与 输出 "World" 这两个语句变成「Go 程」,我们只需要在它们前面加上关键字 go
.
看下面这段代码,这样就算是为 main()
函数添加了两个「Go 程」:
package main
import "fmt"
func main() {
go fmt.Println("Hello")
go fmt.Println("World")
fmt.Println("Say something.")
}
这意味着说,输出 "Hello" 这句语句,和输出 "World" 这句语句会同时启动,说不好哪一句会先输出在屏幕上。
来,运行一下看看:
诶,奇怪!"Hello", "World" 都不再输出了。发生了什么?
原来 main()
函数也是一个「Go 程」。现在输出 "Hello", "World" 的两个函数已经不再是「main Go 程」的一部分,而是独立出去的两个「Go 程」。
于是,fmt.Println("Say something")
不会再等它们,「main Go 程」还没等里面的两个「Go 程」执行完毕,就已经提前结束了。
再于是,在「main go 程」里面的两个「Go 程」再也没有机会执行了。
「Go 程」不能有返回值
正是由于上述的这个特性,Go 语言规定「Go 程」不能有返回值。因为有可能在 main()
函数中已经要用这个返回值来,但是返回它的那个「Go 程」还没执行完毕,这就产生了问题。
那么,「Go 程」是不是就没办法传值了呢?
非也。Go 语言可以定义 channel
. 下面就具体来说说这个 channel
channel
先来看看定义 channel
的语法:
var
+chennel
名 +chan
关键字 + 这个channel
将要保存的值的类型
// 定义 channel
var myChannel chan int
像这样,就定义好了一个保存整型值的 channel
, 它的名字是 myChannel.
和切片、map
一样,定义好之后还不能直接使用,要先 make
:
myChannel = make(chan int)
接下来,这个 myChannel 就可以用来传值了。
具体怎样传值给 channel
呢?用一个 <
加上一个小横线 -
, 把值指向具体的 channel. 就像这样:
myChannel <- 3.14
那又怎么从 channel
中把值拿出来呢?用同样的符号 <-
,把 channel
放到右边:
<-myChannel
实操一下
package main
import "fmt"
func HelloWorld(channel chan string) {
channel <- "Hello"
channel <- "World"
}
func main() {
var myChannel chan string
myChannel = make(chan string)
go HelloWorld(myChannel)
fmt.Println(<-myChannel)
fmt.Println(<-myChannel)
fmt.Println("Say something.")
}
输出结果:
把上面的代码复制到 Go Play 亲自去运行感受一下,学习效果会更佳。
如果对 Go 的语法有基础了解的话,建议按照你对代码工作逻辑的猜想,在此基础上设法删删改改,去验证看看你的猜想是否正确。
channel 的阻塞机制
channel
会通过阻塞来保障它的储值和取值不发生混乱。
同一个 channel
,在接收一个值后就会阻塞。直到这个 channel
中的值被取出——即 <-channel
被执行——才会解除阻塞,继续接收下一个值;
取值后同样会阻塞。直到这个 channel
再次接受新的值,才会解除阻塞,之后才会可以对这个 channel
进行再一次取值。
package main
import (
"fmt"
"time"
)
func HelloWorld(channel chan string) {
channel <- "Hello"
for i := 0; i < 5; i++ {
fmt.Println("channel sleeping")
time.Sleep(time.Second)
}
fmt.Println("channel wakes up!")
channel <- "World"
}
func main() {
myChannel := make(chan string)
go HelloWorld(myChannel)
fmt.Println(<-myChannel)
fmt.Println(<-myChannel)
fmt.Println("Say something.")
}
上面的代码中 HelloWorld()
函数做了一点小改动,在 channel
接收了值 "Hello" 后,会暂停 5 秒,再接着执行接收 "World" 值命令。
从结果中可以看到,我们延缓了 channel
第二次接收值,在 main()
函数中并没有任何延缓执行的命令。但是 main()
函数中的第二次 channel
取值命令被阻塞了。等到 5 秒过去 channel
第二次接收值完成之后,main()
函数中的阻塞才得以解除,于是新接收到的值 "World" 才被第二句 fmt.Println(<-myChannel)
输出出来。
小结
Go 的并发实现简单朴素,两个简单的东西 goroutine
, channel
搞定。像 JavaScript
等类似的语言,往往要花哨和复杂一些。