对GO并发编程的复盘及补充 | 青训营笔记

243 阅读5分钟

这是我参与「第五届青训营」笔记创作活动的第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
}

子协程和主协程调用关系

  1. 在Go语言中,main是主协程,通过go关键字可以实现开启一个子协程。
  2. 开启的子协程和主协程是并行执行的。
  3. 主协程先被触发,子协程后被触发,他们之间会进行资源的争夺
  4. 只有在上一个占据资源的协程释放资源的那一刻,其他的协程才可以重新争夺该资源。
  5. 主协程执行完毕时,等同于进程执行完毕。主协程结束,进程也结束。不会等待子协程执行完毕再结束进程。
  6. 可以通过time.Sleep(1000 * time.Millisecond)释放资源

当令t1、t2代码失效,只保留t3程序运行结果如下

image.png

这意味着主协程和子协程并行执行,但是主协程先被触发且在say1函数执行结束前一直没有释放资源,故只能等待主协程say1函数执行结束并通过time.sleep释放资源后子协程才可以进行资源的抢夺,然后执行say2函数。

当令t1、t3代码生效,t2代码无效程序运行结果如下

image.png

这表明着主协程和子协程并行执行,但是主协程先被触发且在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

image.png

可以发现通道不是自动关闭的

单向通道

上面实现的通道都是双向通道,单向通道顾名思义分为只读和只写两种。

// 定义只读通道
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。

image.png

图中的wg.Add(5)意味着等待子协程done5次后,主协程恢复执行。

小结

  • go中提倡通过通信共享内存,也就是通过通道来实现共享内存。

  • 其他语言常通过内存实现通信。如java常通过lock锁来实现线程间通讯。

image.png

四、总结:

今天对通过对昨天所学知识的复盘对goroutine、waitGroup、channel有了进一步的理解。

五、引用参考: