大家好,我是离散。🌞
关于Golang goroutine和channel的使用,很多人也包括我在内,一开始总是想不清楚它到底是怎么运行的。
问题描述
至于goroutine和channel是什么我就不多说了,如果你不知道那么这个总结你可以先不用看。
直接上代码,关于下面这段代码你觉得会是输出是什么呢?
func main() {
go func() {
res := ""
for i := 0; i < 5; i++ {
res += strconv.Itoa(i)
}
fmt.Println(res)
}()
fmt.Println("Done")
}
很多人会觉得当然是
0 1 2 3 4
Done
然而答案并不是,因为我们的那个sub goroutine还没来得及运行main goroutine就结束了,所以只打印了一个Done整个程序就结束了。
那么有什么办法可以让main goroutine等待sub goroutine跑完才去打印那个Done呢?我们可以让main goroutine先等待个一秒钟。
func main() {
go func() {
res := ""
for i := 0; i < 5; i++ {
res += strconv.Itoa(i) + " "
}
fmt.Println(res)
}()
time.Sleep(1*time.Second)
fmt.Println("Done")
}
但是这样的方式好吗?也许sub goroutine一毫秒就跑完了呢,我们却白白地让main gouroutine等待了足足一秒钟。我们是不是可以让main goroutine在sub goroutine运行完立马就开始运行呢?
当然可以,我们只要搞一个flag让main goroutine去check就行了。
func main() {
flag := false
go func() {
res := ""
for i := 0; i < 5; i++ {
res += strconv.Itoa(i) + " "
}
fmt.Println(res)
flag = true
}()
for !flag {
time.Sleep(1*time.Millisecond)
}
fmt.Println("Done")
}
但是这样又会有一个问题,它是什么呢?
有一句风靡golang社区的话叫做
Do not communicate by sharing memory; instead, share memory by communicating.
不要通过共享内存来通信,而应该通过通信来共享内存。
为什么呢?因为多线程环境下使用共享内存,为了防止一个变量同一时间被多个线程更改,我们需要加锁。这里,我们还只是一个flag,假设有多个flag,很容易出现死锁的情况。
那么就引出了一个channel的概念,Golang中的channel保证同一时间只有一个gotoutine可以访问其中的数据。
但哪怕这样也只是从 用共享内存来通信 换成了 用channel来通信呀,怎么说通过通信来共享内存呢?
channel是可以流通数据的,也就是说不同的线程可以通过channel来交换他们之间的信息,一个发,一个接收就行了。确确实实是通过channel通信的方式来共享了内存哈。
于是上面的问题我们就可以按照如下方式解决:
func main() {
channel := make(chan int)
go func() {
res := ""
for i := 0; i < 5; i++ {
res += strconv.Itoa(i) + " "
}
fmt.Println(res)
channel <- 1
}()
<- channel
fmt.Println("Done")
}
很多人写channel相关的代码很容易就出现一个错误,那就是
fatal error: all goroutines are asleep - deadlock!
比如下面的代码,有的人就会一脸懵逼,为啥我自己存一个拿一个不行呀,怎么就deadlock了呢。
func main() {
channel := make(chan int)
channel <- 1
<- channel
}
关于这个我就三句话总结:
-
channel相当于一个中间站,有自己的库存。channel分为unbuffered channel和buffered channel,前者库存为1,后者库存为初始size加上1
-
当一个goroutine想往channel里面放东西的时候,发现放不下了,这个gouroutine 就会立马阻塞;
-
当一个goroutine想从channel立马取东西的时候,发现里面没有东西,这个goroutine就会立马阻塞。
下面的代码就是我实际工作中review的同事代码
func main() {
wg := sync.WaitGroup{}
wg.Add(2)
channel := make(chan error, 1)
go func() {
defer wg.Done()
channel <- errors.New("err1")
}()
go func() {
defer wg.Done()
channel <- errors.New("err2")
}()
wg.Wait()
close(channel)
select {
case err := <- channel:
if err != nil {
fmt.Println(err)
}
}
}
当时我还问他你这个size只有1不会deadlock吧,我当时的理由是如果sub goroutine1 先运行,然后sub goroutine 2 运行,这不就deadlock了嘛,看完自己之前的总结后我感觉自己是个智障。看来自己总结的东西也要多回顾才行,确实容易忘。
正确的解释是这样,按照刚才的说法,sub goroutine2就会阻塞了,然后main goroutine就会把err拿出来了。
main goroutine这里一定会等待两个sub goroutine运行完才会去运行select那一块的代码。
但需要注意的是,main goroutine永远只会读出一个err就结束了,但这也是我同事的本意,他就是只要看到一个任意err就算全部失败。
总结
-
channel相当于一个中间站,有自己的库存。channel分为unbuffered channel和buffered channel,前者库存为1,后者库存为初始size加上1
-
当一个goroutine想往channel里面放东西的时候,发现放不下了,这个gouroutine 就会立马阻塞;
-
当一个goroutine想从channel立马取东西的时候,发现里面没有东西,这个goroutine就会立马阻塞。
希望读者们都能从这篇文章中学习到一些东西,愿我和我的homie同富有。