最近对Go语言的并发编程有一些新认识和学习,所以想记录下开了这个系列文章
Go语言并发哲学
Do not communicate by sharing memory, instead, share memory by communicating!
不要通过共享内存来通信, 而是通过通信来共享内存!
Go语言最早从UNIX系统的B语言和C语言演化而来,其中的并发特性是从Newsqueak、Alef和Limbo等语言演化而来。
其中Newsqueak是Go之父Rob Pike于989年设计的语言,Alef则是Phil Winterbottom于1993年对C语言扩展了并发特性的语言,Limbo也是Rob Pike参与设计的支持并发的语言。
由此可见,Rob Pike在并发编程语言领域已经积累了几十年的设计经验,Go语言正是站在这些前辈的基础上涅槃重生的。
Go的并发简单又难掌握
func main() {
go println("你好, 并发!")
}
没错 函数前加入 go 关键字即可,当然上面的代码你运行是看不到任何东西的。
上面的程序需要有中彩票特大奖的运气才能有机会执行成功。大部分普通用户将无法看到输出信息!
对于上面的代码有一种很不负责的方法教你调用time.sleep 或 runtime.Gosched 假装解决这个问题:
func main() {
go println("你好, 并发!")
time.Sleep(time.Second) // or runtime.Gosched() 让出CPU时间
}
没有人能够回答为何刚好需要休眠1秒钟就看似能工作了,其实这只是他们尝试后得到的一个经验值。
这种方法可以举个反例就可以推翻他的“不稳定性”:
func main() {
go println("你好, 并发!")
time.Sleep(time.Second)
}
func println(s string) {
time.Sleep(time.Second*2)
print(s+"\n")
}
上面的反例中,我们在不改变println函数输出的前提下重写了实现了改函数,在输出之前先休眠了2秒钟(一定要大于前面的1秒钟)。另一种反面的证明是假设,输出的字符串足够大,输出的设备足够慢,println很可能需要1万年才能完成工作。因为main函数作为println的使用者,不能也无法要求println函数在几个时钟周期内完成任务(毕竟Go语言无法做到实时编程),因此当println函数执行的时间稍微出现波动时就将影响上述代码的正确性!
Go语言并发编程的学习一般要经过2个阶段:
- 第一阶段是这个并发程序终于可以产生正确的输出了;
- 第二个阶段是这个并发程序不会产生错误的输出!
并发问题的解决方案
前面代码运行有一定的随机性,无法保证并发程序的正确运行。导致可能产生错误结果的原因有2个:
- #### 第一个是go启动Goroutine时无法保证新线程马上运行(这里留个坑我后面会填坑的);
- #### 第二个是main函数代表的主Goroutine退出将直接退出进程(语言本身的特性)。
阻止 main 函数退出的方式有很多,就要思路就是让主线程阻塞:
Golang中阻塞的情况包括:
- sync.WaitGroup
一直等待直到WaitGroup等于0。
package main
import "sync"
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
// 内部没有调用 wg.done()
}()
wg.Wait()
}
- 空select
如果一个select{}是一个没有任何case事件的的select,它会一直阻塞下去,只是在程序退出前会报错 死锁问题。
func main() {
runtime.GOMAXPROCS(1)
go println("你好, 并发!")
select {}
}
- 死循环for{}
这样的方式虽然可以阻塞,但会100%占用一个cpu。不建议使用。
func main() {
runtime.GOMAXPROCS(1)
go println("你好, 并发!")
for {}
}
- sync.Mutex 锁
用一个已经锁了的锁,再锁一次就会一直阻塞,这个也不建议使用,但是需要了解,避免这样的坑。
func main() {
var m sync.Mutex
m.Lock()
go println("你好, 并发!")
m.Lock()
}
- os.Signal
os.Signal系统信号量,在golang中是个channel,它在收到特定的消息之前一直阻塞。
func main() {
sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
go println("你好, 并发!")
<-sig
}
- 标准输入os.Stdin
会一直等待控制台输入之前会阻塞。
func main() {
go println("你好, 并发!")
fmt.Scanln()
}
- 空channel或者nil channel
func main() {
c := make(chan struct{})
go println("你好, 并发!")
<-c
}
package main
func main() {
var c chan struct{} //nil channel
go println("你好, 并发!")
<-c
}
理解的解决方案是:main 函数在 println 完成输出任务前不退出,但是在 println 完成任务后可以正确退出。
改进代码如下:
func main() {
done := make(chan bool)
go func() {
println("你好, 并发!")
done <- true
}()
<-done
}
main函数在退出前需要从done管道取一个消息,后台任务在将消息放入done管道前必须先完成自己的输出任务。
因此,main函数成功取到消息时,后台的输出任务确定已经完成了,main函数也就可以放心退出了。