本篇文章将介绍 hello world 的并发实现,其中涉及到的知识有:
在介绍 hello world 的程序实现前,先简要介绍两点: 1. 并发与并行的区别, 2: Go 的 GPM 调度系统
package main
import (
"fmt"
"runtime"
"sync"
)
var wg sync.WaitGroup
func say_hello(value interface{}) {
defer wg.Done()
fmt.Printf("%v", value)
}
func common_say_hello() {
wg.Add(5)
go say_hello("w")
go say_hello("o")
go say_hello("r")
go say_hello("l")
go say_hello("d")
}
func main() {
runtime.GOMAXPROCS(1)
common_say_hello()
wg.Wait()
}
代码介绍:
代码运行结果如下:
dworl
为什么 d 会打印在最前面而 worl 则依次打印呢?<<Go 语言实战>> 给出的解释是“第一个 goroutine 完成所有显示需要花的时间很短,以至于调度器切换到第二个 goroutine之前就完成了所有任务”。那么,这里的第一个 goroutine 是 “go say_hello("d")” 吗?第二个,第三个 goroutine.. 又是哪个呢?调度器根据怎么的顺序来调度 goroutine 的呢?这些问题留给我们后续解答,有知道的朋友还请不吝赐教,感谢。
上面的代码限定了逻辑处理器的数量为 1,所以这里其实实现的是并发而没有并行。当设置逻辑处理器的数量大于 1 时,即实现了并行也实现了并发。更改逻辑处理器数量为 3,查看程序运行情况:
dorlw
dowlr
ldorw
执行了三次每次打印的输出都不一样。
那么是不是到这里就结束了呢?没有。有一点需要说明的是: 一个正在运行的 goroutine 可以被停止并重新调度。如果 goroutine 长时间占用逻辑处理器,调度器会停止该 goroutine,并给其它 goroutine 运行的机会。
基于上述分析,更改 hello world 代码,使得每个 goroutine 占用较长的逻辑处理器时间,查看 goroutine 是否被调度器切换,代码如下:
func multi_hello(prefix string) {
defer wg.Done()
next:
for outer := 2; outer < 5000; outer++ {
for inter := 2; inter < outer; inter++ {
if outer%inter == 0 {
continue next
}
}
fmt.Println("say %s: %d times", prefix, outer)
}
}
func crazy_say_hello() {
wg.Add(5)
go multi_hello("w")
go multi_hello("o")
go multi_hello("r")
go multi_hello("l")
go multi_hello("d")
}
func main() {
runtime.GOMAXPROCS(1)
crazy_say_hello()
wg.Wait()
}
查看代码运行结果:
say r: 4327 times
say r: 4337 times
say r: 4339 times
say w: 4493 times
say w: 4507 times
say w: 4513 times
...
say w: 4999 times
say r: 4349 times
say r: 4357 times
...
这里仅截取部分执行结果。可以看到,第 94-95 行调度器切换 “r goroutine” 到 “w goroutine” ,然后在 99-100 行又从 “w goroutine” 切换到 “r goroutine”。
上述 hello world 的 goroutine 均不涉及对公共资源的访问,因此它们能和谐共存,互不干扰。如果涉及到公共资源的访问,goroutine 将变得相当“野蛮”也即出现相互竞争访问公共资源的状态,这种情况称为“竞争”状态。
进一步的改写 hello world 程序如下:
var helloTimes int32
func cal_hello_num(prefix string) {
defer wg.Done()
value := helloTimes
runtime.Gosched()
value++
helloTimes = value
fmt.Printf("say %s: %d times\n", prefix, helloTimes)
}
func num_say_hello() {
wg.Add(5)
go cal_hello_num("w")
go cal_hello_num("o")
go cal_hello_num("r")
go cal_hello_num("l")
go cal_hello_num("d")
}
func main() {
runtime.GOMAXPROCS(1)
num_say_hello()
wg.Wait()
}
为方便说明这里将逻辑处理器的数量设为 1,同时引入 runtime 包的 Gosched 函数,该函数会将当前 goroutine 从线程退出,并放回到逻辑处理器的队列中。
程序执行结果如下:
say d: 1 times
say w: 1 times
say o: 1 times
say r: 1 times
say l: 1 times
多次执行每个 goroutine 打印结果均为 1,为什么呢?
分析上述代码,每个 goroutine 都会覆盖另一个 goroutine 的工作(竞争状态因此存在)。每个 goroutine 均创造了变量 helloTimes 的副本 value,当 goroutine 切换时每个 goroutine 会将自己维护的 value 赋值给 helloTimes,导致 helloTimes 的值一直是 1。
那么,如果每个 goroutine 都不创造变量的副本是否这种竞争状态就消失了呢?
进一步改写程序如下:
改写版本1
func cal_hello_num(prefix string) {
defer wg.Done()
helloTimes++
runtime.Gosched()
fmt.Printf("say %s: %d times\n", prefix, helloTimes)
}
// 运行结果
say d: 5 times
say w: 5 times
say o: 5 times
say r: 5 times
say l: 5 times
改写版本 2
func cal_hello_num(prefix string) {
defer wg.Done()
runtime.Gosched()
helloTimes++
fmt.Printf("say %s: %d times\n", prefix, helloTimes)
}
// 运行结果
say d: 1 times
say w: 2 times
say o: 3 times
say r: 4 times
say l: 5 times
版本 1 和版本 2 移动了 helloTimes++ 相对于 GoSched 的位置,却得到了完全不同的结果。其实不难理解,因为 helloTimes 是全局变量,每个 goroutine 都维护这个变量。所以,在版本一中每个 goroutine 切换之前都会对全局变量 helloTimes 加 1,加 1 完成后,程序依次打印“最终值” 5。而版本二 goroutine 在切换之后对全局变量加 1,其效果相当于每个 goroutine 按顺序依次执行全局变量的自增操作。
多个 goroutine 访问共享资源极易出现“幺蛾子”,在程序中可以通过锁住共享资源的方式来避免竞争状态的出现。
可以通过原子函数,互斥锁锁住共享资源,实现 goroutine 对共享资源的顺序访问。
未完待续..
原文:https://www.cnblogs.com/xingzheanan/p/14660707.html