Go中的并发是函数相互独立运行的能力,Goroutine是并发运行的函数,Golang提供了Goroutine作为并发操作的一种方式

创造一个协程非常简单,就是在一个任务函数前面加一个go关键字:go task()

原理

goroutine 运行逻辑

:::info 我们通过一个案例来分析协程 goroutine 运行逻辑

:::

协程goroutine - 图1

  1. main是主线程,和协程test同时执行
  2. itoa是将整数转化为字符串

结果:

协程goroutine - 图2

  1. 并发执行
  2. main函数内容先开始执行

流程图:

协程goroutine - 图3

主线程与协程的关系:

要求:协程执行时间B <= 主线程执行时间A

  1. 若B>A,则协程提前随主线程结束
  2. 若B<A,则协程任务完成

:::info 总结

:::

  1. 概念
    1. 主线程
      主线程是-一个物理线程,直接作用在cpu 上的。是重量级的,非常耗费cpu资源。
    2. 协程
      协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
  2. 如何运行?
    1. 并发式运行,宏观上来看,就是主线程和协程交替运行,当然不一定是你一个我一个,可能协程执行两个,主线程执行一个
    2. 谁先运行?
      主线程,why?——是因为我们在创建新的goroutine的时候需要花费一些时间,而此时main函数所在的goroutine是继续执行的。
  3. 主线程与协程的关系→要求(已知主线程执行时间A,协程执行时间B):
    1. 若B>A,则协程提前随主线程结束
    2. 若B<A,则协程任务完成

→要求:B <= A;若B>A,则协程提前随主线程结束

  1. Go优点
    Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了

MPG模型

若能懂操作系统,则更好

:::info 什么是MPG?

:::

协程goroutine - 图4

  1. M: 操作系统的主线程(是物理线程)
  2. P: 协程执行需要的上下文
  3. G:协程

:::info MPG模式运行的状态

:::

  1. 状态1
    协程goroutine - 图5
    1. 并行or并发?——看主线程 Mi 之间关系
      当前程序有三个M, 如果三个M都在一-个cpu运行, 就是并发,如果在不同的cpu运行就是并行
    2. 图解
      M1,M2,M3 正在执行一个G, M1的协程队列有三个,M2的协程队列有3个,M3协程队列有2个
    3. Go优点
      从上图可以看到: Go的协程是轻量级的线程,是逻辑态的,Go可以容易的起上万个协程。其它程序c/java的多 线程,往往是内核态的,比较重量级,几千个线程可能耗光cpu
  2. 状态2
    协程goroutine - 图6
    1. 分成两个部分来看,左边→右边
    2. 左边:
      原来的情况是MO主线程正在执行G0协程,另外有三个协程在队列等待
    3. 情况 如果G0协程阻塞,比如读取文件或者数据库等(即系统不知道你得花多久)
    4. 右边
      这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的G0仍然执行文件io的读写;(有种拆运工的感觉)
    5. 效果
      是并行还是并发,取决于M0和M1的关系;这样的MPG调度模式,可以既让G0执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行。.
    6. 不阻塞
      等到G0不阻塞了,M0会被放到空闲的主线程继续执行(从已有的线程池中取),同时G0又会被唤醒。

协程缺点

  1. 需要估算协程时间
    长了不行,断了输出不出来,数据如果是变化的,那就更难预估时间了
  2. 需要全局变量锁来实现通讯,不利于多个协程多全局变量的读写操作

于是有了新的通讯机制—channel

应用

启动协程goroutine

语法:go 函数

启动单个goroutine

最常遇到的问题就是:协程执行时间B > 主线程执行时间A → 协程提前结束

最简单的解决方法就是:<font style="color:rgb(36, 41, 46);">time.Sleep(time.Second*10)</font> 休息10秒

启动多个goroutine(sync包—锁)

:::info 案例

:::

  1. 需求:
    现在要计算1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。要求使用goroutine完成
  2. 分析:
    1. 使用goroutine 来完成,效率高,但是会出现并发/并行安全问题.
    2. 这里就提出了不同goroutine如何通信的问题

:::info 初步代码实现

:::

  1. package main
  2. import "fmt"
  3. var (
  4. mymap = make(map[int]int, 210)
  5. )
  6. func mycount (n int) {
  7. ret := 1
  8. i := 1
  9. for i<n+1; i++ {
  10. ret *= i
  11. }
  12. mymap[n]=ret
  13. }
  14. func main() {
  15. n := 0
  16. for n<201; n++{
  17. go mycount(n)
  18. }
  19. fmt.Println(mymap)
  20. }

:::info 问题

:::

问题详情 解决方法
协程提前结束问题 不知道协程什么时候结束,怕主线程结束,导致提前结束协程 1. 休眠
2. runtime.Gosched()
安全问题 协程全都在同一个map上修改,如同一百个人在一张纸上写字,有安全问题 1. 全局变量互斥锁
2. 管道channel
int的最大数 int的最大数是 2^63-1 =9,223,372,036,854,775,807 这里很显然,二十多阶乘的时候,超过了 改用unint64

NOTE:-race
在运行某个程序时,如何知道是否存在资源竞争问题。方法很简单,在编译该程序时,增加一个参数-race即可——go build -race 文件名

:::info 解决方案1:使用全局变量锁——排队上厕所

:::

  1. 关于sync包
    1. 记得,通过声明该类型的变量来调用,这里我们设变量名为lock,类型是sync.Mutex,然后我们就可以引用这个方法;
    2. 关于sync包 协程goroutine - 图7
  2. 流程图
    协程goroutine - 图8

    协程goroutine - 图9
  3. 我们来看代码实现
  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. var (
  8. mymap = make(map[int]int, 21)
  9. lock sync.Mutex
  10. )
  11. func mycount (n int) {
  12. ret := 1
  13. for i:=1;i<n+1; i++ {
  14. ret *= i
  15. }
  16. lock.Lock()
  17. mymap[n]=ret
  18. lock.Unlock()
  19. }
  20. func main() {
  21. for i := 0;i<20; i++ {
  22. go mycount(i+1)
  23. }
  24. time.Sleep(time.Second*5)
  25. defer func() {
  26. for i,v := range mymap{
  27. fmt.Printf("map[%d]=%d\n", i, v)
  28. }
  29. }()
  30. }

协程goroutine - 图10

:::info 解决方法2:管道channel

:::

见channel章节

runtime包

Gosched 最后一个接力棒

  1. 语法:<font style="color:rgb(0,0,0);">runtime.Gosched()</font>
  2. 作用 1. runtime.Gosched() 用于让出 CPU 时间片,让出当前 goroutine 的执行权限,调度器安排其他 等待的任务运行,并在下次某个时候从该位置恢复执行。 类似于defer 2. 比喻:
    这就像跑接力赛,A 跑了一会碰到代码 runtime.Gosched() 就把接力棒交给 B 了,A 歇着了, B 继续跑。

:::info 代码

:::

协程goroutine - 图11

结果:

协程goroutine - 图12

Goexit 终止协程

  1. 语法:<font style="color:rgb(0,0,0);">runtime.Goexit()</font>
  2. 作用:
    调用 runtime.Goexit() 将立即终止当前 goroutine 执⾏,但调度器会确保所有已注册 defer 延迟调用被执行。(注意是已注册哦,也就是要在<font style="color:rgb(0,0,0);">runtime.Goexit()</font>之前,这一点我已经验证)

:::info 代码实现对比

:::

  1. 情况1:协程goroutine - 图13结果:
    协程goroutine - 图14
  2. 情况2:加一个return,终止此函数
    协程goroutine - 图15
    结果:
    协程goroutine - 图16
  3. 情况三:把return 改成 runtime.Goexit,终止所在协程(即便有函数套娃也阻挡不了)
    协程goroutine - 图17
    结果:
    协程goroutine - 图18

NUMCPU/GOMAXPROCS—查询/设置CPU数量

  1. 包: runtime
  2. 语法+作用:
    1. 查询并获取CPU的数量——函数 runtime.NUMCPU()
    2. 设置CPU运行的数量——方法runtime.COMAXPROCS(n)n为要运行的CPU的数量,目的在于程序员能根据目前的任务量来分配硬件资源
  3. go 1.8版本
    1. go1.8后,默认让程序运行在多个核上,可以不用设置了
    2. go1.8前,还是要设置一下,可以更高效的利益cpu

:::info 实例

:::

协程goroutine - 图19

使用WaitGroup实现同步

等待组waitGroup

排队,执行,再去执行其他线程

语法:

  1. 声明变量var wp sync.WaitGGroup,来自于一个包sync(这个包的意思就是同步)
  2. 过程
    1. 启动一个goroutine就登记+1,就wp.Add(1)
    2. goroutine结束就登记-1,,就wp.Done()
    3. wp.Wait(),等待所有登记的goroutine都结束,判断线程是不是=0,若为0就执行下一个线程

:::info 实例

:::

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. var wg sync.WaitGroup
  7. func hello(i int){
  8. defer wg.Done() //goroutine结束就登记-1,defer保证函数结束前才进行
  9. fmt.Printf("你好我是第%d个\n", i)
  10. }
  11. func main() {
  12. for i:=0; i<10; i++{
  13. wg.Add(1) //启动一个goroutine就登记+1
  14. go hello(i)
  15. }
  16. wg.Wait() //等待所有登记的goroutine都结束,最终=0
  17. fmt.Println("主线程结束")
  18. }

协程goroutine - 图20

结果

协程goroutine - 图21