Go(Golang)_11_并发/并行

并发/并行

基础概念

进程(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)运行结果;
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值