并发/并行
基础概念
进程(Process):提供程序在运行期间所需用到各种资源的容器
1)提供的资源主要为:内存地址空间、文件/设备的句柄和线程;
2)每个进程至少包含一个线程(主线程);
3)当主线程终止时,进程也随之终止;
线程(Thread):进程中的可执行空间(内核态线程)
1)用于被操作系统执行特定的函数;
2)操作系统的线程调度器会将线程调度到特定处理器上运行;
如:系统中程序运行后进程和线程的联系
协程(Coroutine):用户态线程
1)协程在用户态下完成切换,减少切换上下文的资源消耗;
2)协程调度方式为:协作式(线程为抢占式);
3)协程必须绑定在线程上才可运行;
如:线程和协程相对CPU
//线程和进程的绑定关系分为: 1:1、 N:1、 M:N
Go中系统处理器(CPU)分为:物理处理器、逻辑处理器
物理处理器:硬件实际安装的CPU
1)默认为每个可用的物理处理器分为一个逻辑处理器
逻辑处理器:通过物理处理器模拟处的CPU
1)Go协程调度器会将goroutine调度到特定的逻辑处理器上运行
2)每个逻辑处理器都分别绑定到单个操作系统进程;
goroutine
goroutine(Go协程):Go语言中实现的协程(默认并发运行)
1)存储goroutine的栈是动态缩放的(2KB~1GB);
2)正在运行的goroutine在正常结束前,可被停止并重新调度;
3)多个goroutine之间轮流在绑定的线程中执行(直到结束,退出运行队列);
//重新调度和轮流执行保证每个goroutine可执行,且不会长时间占用逻辑处理器
goroutine的定义格式分为两种:基于匿名函数、基于已定义函数
(1)基于匿名函数:
go func(参数列表) {
程序段
}(传入参数列表)
(2)基于已定义函数(包含函数变量):
go 函数名/函数变量(传入参数)
编译器在编译时会为main()函数创建默认的goroutine(主goroutine);
1)当主goroutine退出或崩溃时,属于主goroutine的所有协程会被强制终结;
2)可通过runtime包中的Wait()方法确保主goroutine等待其他协程的完成;
runtime.GOMAXPROCS(N):占用系统中N个调度器以运行goroutine
1)若不指定,则默认值为系统已有的CPU数量(物理处理器);
2)正在休眠和倍通道阻塞的goroutine不会占用该数量;
3)I/O阻塞、系统调用和非Go编写的函数需占用独立线程(不算在N之中);
//默认一个线程可运行多个goroutine(根据goroutine调度器的配置)
如:调用goroutine的栈中变量
1)编写程序;
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
var wg sync.WaitGroup
wg.Add(10)
data := make(map[int]int, 10)
for i := 1; i <= 10; i++ {
data[i] = i
}
for key, value := range data {
go func(key, value int) { //goroutine栈中变量的刷新频率是不固定的
defer wg.Done() //通过参数传递强制goroutine刷新其栈中的变量
//也可通过重定义变量以刷新栈中变量
fmt.Println("K:", key, "V:", value)
}(key, value)
}
wg.Wait()
}
2)运行结果;
并发
并发(Concurrency):处理器按照运行队列同时处理多个任务(逻辑上)
1)Go语言中的并发基于CSP实现;
2)Go中的goroutine默认实现并发运行;
//通信顺序进程(communicating Sequential Processes,CSP
):消息传递模型;
goroutine创建后的调度流程(并发模型):
1)创建goroutine,并将该goroutine放到协程调度器的全局运行队列中;
2)协程调度器将队列中的goroutine分配给特定的逻辑处理器;
3)并将goroutine插入到逻辑处理器的本地运行队列中;
4)goroutine在本地运行队列中等待被逻辑处理器允许;
//若该goroutine会阻塞,则会单独分配一个进程以运行该goroutine
如:协程调度器的工作流程(并发模型)
如:通过两个goroutine分别输出大小写的26个字母(并发模型)
1)编写程序;
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
runtime.GOMAXPROCS(1) //指定逻辑处理器的数量
var wg sync.WaitGroup
wg.Add(2) //指定可用于非陪给goroutine的计数量
fmt.Println("Start Goroutines")
go func() {
defer wg.Done()
for count := 0; count < 3; count++ {
for char := 'a'; char < 'a'+26; char++ {
fmt.Printf("%c", char)
}
fmt.Printf("\n")
}
}()
go func() {
defer wg.Done()
for count := 0; count < 3; count++ {
for char := 'A'; char < 'A'+26; char++ {
fmt.Printf("%c", char)
}
fmt.Printf("\n")
}
}()
fmt.Println("Waiting To Finish")
wg.Wait() //判断计数量是否大于0(大于时会阻塞主gourtine)
fmt.Println("\nFinsh ALL")
}
2)运行结果;
并行
并行(Parallelism):多个处理器按照各自运行队列同时处理多个任务(物理上)
1)Go中实现并发运行,必须存在2个或2个以上的逻辑处理器;
2)协程调度器会将创建的goroutine平等分配到每个逻辑处理器上运行;
如:协程调度器的工作流程(并行流程)
//系统必须拥有多个物理处理器,否则本质上还是在同一物理处理器上并发运行
如:通过两个goroutine分别输出大小写的26个字母(并行模型)
1)编写程序;
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU()) //指定逻辑处理器的数量
//NumCPU()函数返回系统中所有可用的物理处理器数量(实现并行)
var wg sync.WaitGroup
wg.Add(2) //指定可用于非陪给goroutine的计数量
fmt.Println("Start Goroutines")
go func() {
defer wg.Done()
for count := 0; count < 3; count++ {
for char := 'a'; char < 'a'+26; char++ {
fmt.Printf("%c", char)
}
fmt.Printf("\n")
}
}()
go func() {
defer wg.Done()
for count := 0; count < 3; count++ {
for char := 'A'; char < 'A'+26; char++ {
fmt.Printf("%c", char)
}
fmt.Printf("\n")
}
}()
fmt.Println("Waiting To Finish")
wg.Wait() //判断计数量是否大于0(大于时会阻塞主gourtine)
fmt.Println("\nFinsh ALL")
}
2)运行结果;
goroutine调度
GMP模型:调度器和工作线程绑定以从队列中获取goroutine运行
1)G(goroutine)、M(工作线程)、P(协程调度器);
2)Hand Off机制:当M运行的G阻塞时,会释放P转移其他空闲的M;
3)Work Stealing机制:当M无可运行的G时,会偷取其他M的G(不销毁);
//P与M的比例关系只能是1:1(M必须持有P才可执行G)
如:GMP模型实现原理图
1)全局队列(Global Queue,GQ):存储等待运行的G;
2)P的本地队列:存储等待运行的G,但仅能存储256个(超出存储于GQ);
3)P队列:程序启动时创建,并存储在数组中(最多GOMAXPROCS个)
//若P的本地队列超过256个,则会将其中前一半的G转移到GQ
//每调用P的本地队列中61个G后,就从GQ中调用个G再继续
协程调度器的工作流程:
1)M获取P,从P的本地队列中获取G运行;
2)若P的本地队列为空,M从GQ获取一部分G放在P的本地队列中;
3)M也有可能从其他P的本地队列中偷一半放到自己的P的本地队列中;
//M与P的数量没有绝对关系;当M被阻塞时P会切换或创建新的M
如:协程调度器的工作流程图
调度器生命周期
运行程序时,其协程调度器实现的流程:
1)runtime包创建线程M0和G0,并关联两者;
2)初始化调度器:初始化M0、栈、GC以及创建P0列表;
3)runtime.main调用main.main,为runtime.main创建GR加入P0的本地队列;
4)启动M0并绑定P0,从P0的本地队列中获取GR运行设置运行环境;
5)为main.main创建GM并加入到P的本地队列中(并创建线程M);
6)M循环执行P的本地队列直到GM运行完成;
//GM运行完成时,GM会执行Defer和Panic以结束程序
如:协程调度器工作流程图
调度器注意事项
(1)当存储G的数量超过P的本地队列的容量,则会将其中一半存储于GQ;
1)后需创建会继续尝试先在P的本地队列中添加;
2)如:P的本地队列容量为4个,但添加5个G时
(2)创建G时,G会尝试唤醒睡眠的线程
1)自旋线程:P和M绑定且为运行状态,但没G运行(不断寻找G);
2)必须有空闲的P才可创建自旋线程;
3)如:P1的本地队列G2唤醒线程并创建自旋线程
//若GQ中有G,自旋线程则会从中获取G(结束自旋状态)
(4)若GQ没G,自旋线程会尝试从其他的本地队列中偷取G
1)每次偷取数量为该本地队列的一半;
2)如:P2从P1的本地队列中偷取G以结束自旋状态
//若其他所有本地队列均为空,则继续保持自旋状态
(5)若G会发生阻塞,则在运行该G时会释放P
1)释放的P会与其他的M绑定,并继续运行P的本地队列中的G;
2)如:P2的G8发生阻塞,导致M2与P2解绑
(6)程序中goroutine的执行顺序和创建顺序并不完全相同
1)原因1:P的runnext字段指向程序中最后次创建的goroutine;
2)原因2:P本地队列中调用61个G后,就从GQ中调用个G再继续;
//runnext字段每次都指向最新创建的G(被代替的G进入本地队列)
如:顺序创建3个goroutine,并输出其创建次数
1)编写程序
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
runtime.GOMAXPROCS(1)
var wg sync.WaitGroup
for i := 1; i <= 3; i++ { // 也可指定循环次数为258观察原因2的现象
wg.Add(1)
go func(i int) {
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}
2)运行结果;