Go中的并发是函数相互独立运行的能力,Goroutine是并发运行的函数,Golang提供了Goroutine作为并发操作的一种方式
创造一个协程非常简单,就是在一个任务函数前面加一个go关键字:go task()
原理
goroutine 运行逻辑
:::info 我们通过一个案例来分析协程 goroutine 运行逻辑
:::
- main是主线程,和协程test同时执行
- itoa是将整数转化为字符串
结果:
- 并发执行
- main函数内容先开始执行
流程图:
主线程与协程的关系:
要求:协程执行时间B <= 主线程执行时间A
- 若B>A,则协程提前随主线程结束
- 若B<A,则协程任务完成
:::info 总结
:::
- 概念
- 主线程
主线程是-一个物理线程,直接作用在cpu 上的。是重量级的,非常耗费cpu资源。 - 协程
协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
- 主线程
- 如何运行?
- 并发式运行,宏观上来看,就是主线程和协程交替运行,当然不一定是你一个我一个,可能协程执行两个,主线程执行一个
- 谁先运行?
主线程,why?——是因为我们在创建新的goroutine的时候需要花费一些时间,而此时main函数所在的goroutine是继续执行的。
- 主线程与协程的关系→要求(已知主线程执行时间A,协程执行时间B):
- 若B>A,则协程提前随主线程结束
- 若B<A,则协程任务完成
→要求:B <= A;若B>A,则协程提前随主线程结束
- Go优点
Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了
MPG模型
若能懂操作系统,则更好
:::info 什么是MPG?
:::
- M: 操作系统的主线程(是物理线程)
- P: 协程执行需要的上下文
- G:协程
:::info MPG模式运行的状态
:::
- 状态1
- 并行or并发?——看主线程 Mi 之间关系
当前程序有三个M, 如果三个M都在一-个cpu运行, 就是并发,如果在不同的cpu运行就是并行 - 图解
M1,M2,M3 正在执行一个G, M1的协程队列有三个,M2的协程队列有3个,M3协程队列有2个 - Go优点
从上图可以看到: Go的协程是轻量级的线程,是逻辑态的,Go可以容易的起上万个协程。其它程序c/java的多 线程,往往是内核态的,比较重量级,几千个线程可能耗光cpu
- 并行or并发?——看主线程 Mi 之间关系
- 状态2
- 分成两个部分来看,左边→右边
- 左边:
原来的情况是MO主线程正在执行G0协程,另外有三个协程在队列等待 - 情况 如果G0协程阻塞,比如读取文件或者数据库等(即系统不知道你得花多久)
- 右边
这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的G0仍然执行文件io的读写;(有种拆运工的感觉) - 效果
是并行还是并发,取决于M0和M1的关系;这样的MPG调度模式,可以既让G0执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行。. - 不阻塞
等到G0不阻塞了,M0会被放到空闲的主线程继续执行(从已有的线程池中取),同时G0又会被唤醒。
协程缺点
- 需要估算协程时间
长了不行,断了输出不出来,数据如果是变化的,那就更难预估时间了 - 需要全局变量锁来实现通讯,不利于多个协程多全局变量的读写操作
于是有了新的通讯机制—channel
应用
启动协程goroutine
语法:go 函数
启动单个goroutine
最常遇到的问题就是:协程执行时间B > 主线程执行时间A → 协程提前结束
最简单的解决方法就是:<font style="color:rgb(36, 41, 46);">time.Sleep(time.Second*10)</font>
休息10秒
启动多个goroutine(sync包—锁)
:::info 案例
:::
- 需求:
现在要计算1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。要求使用goroutine完成 - 分析:
- 使用goroutine 来完成,效率高,但是会出现并发/并行安全问题.
- 这里就提出了不同goroutine如何通信的问题
:::info 初步代码实现
:::
package main
import "fmt"
var (
mymap = make(map[int]int, 210)
)
func mycount (n int) {
ret := 1
i := 1
for i<n+1; i++ {
ret *= i
}
mymap[n]=ret
}
func main() {
n := 0
for n<201; n++{
go mycount(n)
}
fmt.Println(mymap)
}
:::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:使用全局变量锁——排队上厕所
:::
- 关于sync包
- 记得,通过声明该类型的变量来调用,这里我们设变量名为lock,类型是sync.Mutex,然后我们就可以引用这个方法;
- 关于sync包
- 流程图
→ - 我们来看代码实现
package main
import (
"fmt"
"sync"
"time"
)
var (
mymap = make(map[int]int, 21)
lock sync.Mutex
)
func mycount (n int) {
ret := 1
for i:=1;i<n+1; i++ {
ret *= i
}
lock.Lock()
mymap[n]=ret
lock.Unlock()
}
func main() {
for i := 0;i<20; i++ {
go mycount(i+1)
}
time.Sleep(time.Second*5)
defer func() {
for i,v := range mymap{
fmt.Printf("map[%d]=%d\n", i, v)
}
}()
}
:::info 解决方法2:管道channel
:::
见channel章节
runtime包
Gosched 最后一个接力棒
- 语法:
<font style="color:rgb(0,0,0);">runtime.Gosched()</font>
- 作用
1. runtime.Gosched() 用于让出 CPU 时间片,让出当前 goroutine 的执行权限,调度器安排其他 等待的任务运行,并在下次某个时候从该位置恢复执行。 类似于defer
2. 比喻:
这就像跑接力赛,A 跑了一会碰到代码 runtime.Gosched() 就把接力棒交给 B 了,A 歇着了, B 继续跑。
:::info 代码
:::
结果:
Goexit 终止协程
- 语法:
<font style="color:rgb(0,0,0);">runtime.Goexit()</font>
- 作用:
调用 runtime.Goexit() 将立即终止当前 goroutine 执⾏,但调度器会确保所有已注册 defer 延迟调用被执行。(注意是已注册哦,也就是要在<font style="color:rgb(0,0,0);">runtime.Goexit()</font>
之前,这一点我已经验证)
:::info 代码实现对比
:::
- 情况1:结果:
- 情况2:加一个return,终止此函数
结果: - 情况三:把return 改成
runtime.Goexit
,终止所在协程(即便有函数套娃也阻挡不了)
结果:
NUMCPU/GOMAXPROCS—查询/设置CPU数量
- 包:
runtime
- 语法+作用:
- 查询并获取CPU的数量——函数
runtime.NUMCPU()
- 设置CPU运行的数量——方法
runtime.COMAXPROCS(n)
n为要运行的CPU的数量,目的在于程序员能根据目前的任务量来分配硬件资源
- 查询并获取CPU的数量——函数
- go 1.8版本
- go1.8后,默认让程序运行在多个核上,可以不用设置了
- go1.8前,还是要设置一下,可以更高效的利益cpu
:::info 实例
:::
使用WaitGroup实现同步
等待组waitGroup
排队,执行,再去执行其他线程
语法:
- 声明变量
var wp sync.WaitGGroup
,来自于一个包sync(这个包的意思就是同步) - 过程
- 启动一个goroutine就登记+1,就
wp.Add(1)
- goroutine结束就登记-1,,就
wp.Done()
wp.Wait()
,等待所有登记的goroutine都结束,判断线程是不是=0,若为0就执行下一个线程
- 启动一个goroutine就登记+1,就
:::info 实例
:::
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func hello(i int){
defer wg.Done() //goroutine结束就登记-1,defer保证函数结束前才进行
fmt.Printf("你好我是第%d个\n", i)
}
func main() {
for i:=0; i<10; i++{
wg.Add(1) //启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() //等待所有登记的goroutine都结束,最终=0
fmt.Println("主线程结束")
}
结果