这是我参与「第五届青训营」笔记创作活动的第3天。
一、内容:
在昨天的学习中,对Go并发编程有了初步的了解,但是对子协程和主协程的关系理解的不透彻。在今天的学习中通过一些案例进一步理解了子协程和主协程的关系,以及waitGroup,通信共享内存。
二、一个例子彻底明白Go语言中子协程和主协程的调用关系
package main
import (
"fmt"
"time"
)
func say1() {
fmt.Println("hello")
time.Sleep(1000 * time.Millisecond) //t1
fmt.Println("hello")
fmt.Println("hello")
}
func say2() {
time.Sleep(2000 * time.Millisecond) //t2
fmt.Println("world")
fmt.Println("world")
fmt.Println("world")
}
func main() {
go say2()
say1()
time.Sleep(2000 * time.Millisecond) //t3
}
子协程和主协程调用关系
- 在Go语言中,main是主协程,通过go关键字可以实现开启一个子协程。
- 开启的子协程和主协程是并行执行的。
- 主协程先被触发,子协程后被触发,他们之间会进行资源的争夺。
- 只有在上一个占据资源的协程释放资源的那一刻,其他的协程才可以重新争夺该资源。
- 主协程执行完毕时,等同于进程执行完毕。主协程结束,进程也结束。不会等待子协程执行完毕再结束进程。
- 可以通过time.Sleep(1000 * time.Millisecond)释放资源
当令t1、t2代码失效,只保留t3程序运行结果如下
这意味着主协程和子协程并行执行,但是主协程先被触发且在say1函数执行结束前一直没有释放资源,故只能等待主协程say1函数执行结束并通过time.sleep释放资源后子协程才可以进行资源的抢夺,然后执行say2函数。
当令t1、t3代码生效,t2代码无效程序运行结果如下
这表明着主协程和子协程并行执行,但是主协程先被触发且在say1函数执行至time.Sleep(1000 * time.Millisecond)前一直没有释放资源,在执行至time.Sleep(1000 * time.Millisecond)时释放资源,子协程在此时抢夺资源并执行say2函数,sqy2函数执行结束以后,子协程释放资源,同时主协程抢夺资源,然后继续执行之前主协程没有执行完的代码。
三、对通信共享内存的深度理解
实现一个简单的无缓冲通道
func main() {
var ch chan string = make(chan string)
fmt.Println("1")
go PrintChan(ch)
res := <-ch // 发生了阻塞,等待ch返回结果后才会接收数据并向下执行
fmt.Println(res)
println("3")
}
func PrintChan(c chan string) {
c <- "2" //向通道中发送数据 "2"
}
这里的通道由于没设置长度,故为一个无缓冲通道。这种无缓冲通道需要等到有两个不同的协程(包括主进程)来同时有存和取时才可以执行。否则将一直阻塞或者产生死锁。
检查通道是否关闭
close(ch)
value, ok := <-ch // 如果通道已经关闭,则ok为false
可以发现通道不是自动关闭的
单向通道
上面实现的通道都是双向通道,单向通道顾名思义分为只读和只写两种。
// 定义只读通道
type Receiver = <-chan int
receiver := make(Receiver)
// 定义只写通道
type Sender = chan<- int
sender := make(Sender)
遍历通道
常用range来遍历通道。在往通道中写入数据结束以后记得要关闭通道,否则主协程遍历完不会结束,而会阻塞。
package main
import (
"fmt"
)
func loopPrint(c chan int) {
for i := 0; i < 10; i++ {
c <- i
}
close(c)
}
func main() {
// 创建一个通道
var ch2 = make(chan int, 5)
go loopPrint(ch2)
for v := range ch2 {
fmt.Println(v)
}
}
用通道做锁
当通道容量为 1 时,说明通道只能缓存一个数据,若通道中已有一个数据,此时再往里发送数据,会造成程序阻塞。也就起到了锁的效果。
死锁
死锁情景:
1.当协程给一个无缓冲通道发送数据时,照理说会有其他 Go 协程来接收数据。如果没有的话,程序就会在运行时触发 panic ,形成死锁。同理,当有协程等着从一个通道接收数据时,我们期望其他的 Go 协程会向该通道写入数据,要不然程序也会触发 panic 。
2.定义了缓冲通道的容量,但通道里的容量已经放不下新的数据,而没有接收者接收数据,就会造成阻塞,而对于一个协程来说就会造成死锁。
3.当程序一直在等待从缓冲通道里读取数据,但此时已经没有发送者往通道中写入数据,当通道中的数据已经被耗尽但仍读取,也会造成死锁。
waitGroup
在实际开发中我们并不能保证每个协程执行的时间,如果需要等待多个协程,全部结束任务后,再执行某个业务逻辑这时就要用到waitGroup。
图中的wg.Add(5)意味着等待子协程done5次后,主协程恢复执行。
小结
-
go中提倡通过通信共享内存,也就是通过通道来实现共享内存。
-
其他语言常通过内存实现通信。如java常通过lock锁来实现线程间通讯。
四、总结:
今天对通过对昨天所学知识的复盘对goroutine、waitGroup、channel有了进一步的理解。