wordpress 网站图标,电子商务网站的定义,网站充值链接怎么做,学校网站备案前置审批前些天发现了一个巨牛的人工智能学习网站#xff0c;通俗易懂#xff0c;风趣幽默#xff0c;忍不住分享一下给大家#xff1a;点击跳转到网站#xff0c;对人工智能感兴趣的小伙伴可以点进去看看。
前言
本章是Go并发编程的起始篇章#xff0c;在未来几篇文章中我们会…前些天发现了一个巨牛的人工智能学习网站通俗易懂风趣幽默忍不住分享一下给大家点击跳转到网站对人工智能感兴趣的小伙伴可以点进去看看。
前言
本章是Go并发编程的起始篇章在未来几篇文章中我们会围绕Go并发编程进行理论和实战的学习欢迎关注我哦
本章主要以介绍GMP模型为主偏向于面试和八股目的是让小伙伴们注重于知识本身面向面试面向八股面向加薪。
Go语言自诞生以来就以其简洁、高效的并发模型著称。而这其中的核心正是GMP模型。理解GMP模型的演进历程能帮助我们更好地掌握Go的并发编程。而Goroutine作为Go中的核心概念极大地简化了并发编程的复杂度。本文将详细阐述Go语言GMP模型的演变过程并深入解析其设计理念和优点并详细介绍Goroutine的基本概念、优势及其使用方法并结合具体代码示例进行说明。
面试题目
在阅读本文前先带着以下几个关于GMP模型的面试题目进行思考以加深理解和掌握
什么是GMP模型请解释其基本概念。
回答要点解释G、M、P的概念及其在调度模型中的角色。
如何理解GMP模型中线程的内核态和用户态
回答要点区分内核态线程和用户态线程并说明它们在GMP模型中的作用。
Go语言中的Goroutine与线程的映射关系是怎样的为什么选择这种映射方式
回答要点解释Goroutine与线程的多对多映射关系及其优点。
GMP模型如何解决线程调度中的锁竞争问题
回答要点介绍全局队列和本地队列的使用以及G的分配机制。
GMP模型中的Stealing机制是什么它如何工作
回答要点描述Stealing机制的原理及其在Goroutine调度中的应用。
什么是Hand off机制在什么情况下会使用该机制
回答要点解释Hand off机制及其在阻塞和系统调用中的应用。
如何理解GMP模型中的抢占式调度它解决了哪些问题
回答要点说明抢占式调度的原理及其在防止协程饿死中的作用。
什么是G0和M0它们在GMP模型中扮演什么角色
回答要点描述G0和M0的定义及其在Goroutine调度中的功能。
请详细说明GMP模型中的调度策略。
回答要点逐步解释Goroutine的创建、唤醒、偷取、切换、自旋、系统调用和阻塞处理策略。
如何在实际项目中调优GMP调度模型
回答要点讨论如何通过调整GOMAXPROCS等参数来优化调度性能。
带着这些问题阅读本文可以帮助你更系统地掌握GMP模型的核心概念和调度机制提高面试中的应答能力。
单进程时代
基本概念
在单进程时代一个进程就是一个运行中的程序。计算机系统在执行程序时会从头到尾依次执行完一个程序然后再执行下一个程序。在这种模型中不需要复杂的调度机制因为只有一个执行流程。
面临的两个问题
单一执行流程由于只能一个个执行程序无法同时处理多个任务这大大限制了CPU的利用率。进程阻塞当一个进程遇到I/O操作等阻塞情况时CPU资源会被浪费等待进程完成阻塞操作后再继续执行导致效率低下。
多进程/线程并发时代
基本概念
为了解决单进程时代的效率问题引入了多进程和多线程并发模型。在这种模型中当一个进程阻塞时CPU可以切换到另一个准备好的进程继续执行。这样可以充分利用CPU资源提高系统的并发处理能力。
两个问题
高开销进程拥有大量资源进程的创建、切换和销毁都需要消耗大量的时间和资源。这导致CPU很大一部分时间都在处理进程调度而不是实际的任务执行。高内存占用在32位机器下进程的虚拟内存占用为4GB线程占用为4MB。大量的线程和进程会导致高内存消耗限制了系统的扩展性。
协程的引入
为了解决多进程和多线程带来的高开销和高内存占用问题引入了协程Coroutine。协程是一种比线程更轻量级的执行单元。协程在用户态进行调度避免了频繁的上下文切换带来的开销。Go语言的GMP模型正是基于协程的设计。
协程的基本概念
在深入了解Goroutine之前先来了解一下协程Coroutine的基本概念。
内核态和用户态
内核态线程由操作系统管理和调度CPU只负责处理内核态线程。用户态线程由用户程序管理需绑定到内核态线程上执行协程即为用户态线程的一种。 内核态和用户态线程关系图
Kernel Space内核空间上半部分的灰色区域表示操作系统管理的内核空间。User Space用户空间下半部分的白色区域表示用户程序运行的空间。Kernel Thread 1 和 Kernel Thread 2内核线程由操作系统管理的内核线程CPU直接处理这些线程。User Thread 1、User Thread 2 和 User Thread 3用户线程由用户程序管理的用户线程协程需绑定到内核线程上执行。
执行流程 用户态线程 用户程序创建多个用户线程如协程如图中的“User Thread 1”、“User Thread 2”和“User Thread 3”。 内核态线程 用户线程需绑定到内核态线程上执行如图中的“Kernel Thread 1”和“Kernel Thread 2”。 CPU处理 CPU只处理内核态线程通过绑定关系用户态线程的执行也依赖于内核态线程的调度。图中的红色箭头表示CPU正在处理内核线程从而间接处理绑定的用户线程。
线程和协程的映射关系
单线程绑定所有协程 问题1无法利用多核CPU的能力。问题2如果某个协程阻塞整个线程和进程都将阻塞导致其他协程无法执行丧失并发能力。 一对一映射 将每个协程绑定到一个线程上退回到多进程/线程的模式协程的创建、切换、销毁均需CPU完成效率低下。 多对多映射 允许多个协程绑定到多个线程上形成M:N的关系。这样可以充分利用多核CPU并通过协程调度器高效管理协程的执行。 Goroutine
Goroutine是Go语言中的协程实现了轻量级并发。与传统的线程相比Goroutine具有以下显著特点
轻量级
Goroutine非常轻量初始化时仅占用几KB的栈内存并且栈内存可以根据需要动态伸缩。这使得我们可以在Go程序中创建成千上万个Goroutine而不会消耗过多的系统资源。
高效调度
Goroutine的调度由Go语言的运行时runtime负责而不是操作系统。Go运行时在用户态进行调度避免了频繁的上下文切换带来的开销使得调度更加高效。
Goroutine的使用示例
下面是一个简单的示例展示了如何在Go语言中使用Goroutine进行并发编程。
package mainimport (fmttime
)func say(s string) {for i : 0; i 5; i {time.Sleep(100 * time.Millisecond)fmt.Println(s)}
}func main() {go say(Hello)go say(World)time.Sleep(1 * time.Second)fmt.Println(Done)
}在这个示例中两个Goroutine同时执行分别打印Hello和World。通过使用go关键字我们可以轻松地启动一个新的Goroutine。
需要注意的事项
主Goroutine的结束在Go程序中main函数本身也是一个Goroutine称为主Goroutine。当主Goroutine结束时所有其他Goroutine也会随之终止。因此需要确保主Goroutine等待所有子Goroutine执行完毕。同步和共享数据虽然Goroutine之间共享内存空间但需要通过同步机制如通道和锁来避免竞争条件。Go语言推荐使用通道channel进行Goroutine之间的通信以保证数据的安全性和同步性。
示例使用通道进行同步
下面的示例展示了如何使用通道来同步多个Goroutine的执行。
package mainimport (fmtsync
)func worker(id int, wg *sync.WaitGroup) {defer wg.Done()fmt.Printf(Worker %d starting\n, id)// 模拟工作fmt.Printf(Worker %d done\n, id)
}func main() {var wg sync.WaitGroupfor i : 1; i 5; i {wg.Add(1)go worker(i, wg)}wg.Wait()fmt.Println(All workers done)
}在这段代码中使用sync.WaitGroup来同步多个Goroutine。主Goroutine启动多个子Goroutine并等待它们完成每个子Goroutine在完成任务后调用wg.Done()减少计数主Goroutine调用wg.Wait()阻塞等待所有子Goroutine完成。
执行流程
主Goroutine启动多个子GoroutineGoroutine 1、2、3。各个Goroutine并发执行它们的任务。每个Goroutine在完成任务后向通道发送信号表示已完成。主Goroutine通过通道接收所有子Goroutine的完成信号然后继续执行。 Goroutine执行与同步流程图
这张图展示了多个Goroutine同时执行的流程以及如何通过通道Channel进行同步。
Goroutine 1、2、3代表多个并发执行的Goroutine分别标记为“Goroutine 1”、“Goroutine 2”和“Goroutine 3”。Main Goroutine主Goroutine它负责启动其他Goroutine并等待它们完成。Channel用于同步Goroutine的通道。
关于waitgroup我会在下一章节中进行详细讲解欢迎订阅我的频道在本实例代码中大家了解使用即可。
Goroutine调度器
基本概念
在Go中线程是运行Goroutine的实体而调度器的功能是将可运行的Goroutine分配到工作线程上。Go语言采用了一种高效的Goroutine调度机制使得程序能够在多核处理器上高效运行。
被废弃的调度器
早期的调度器采用了简单的设计存在多个缺陷 概念用大写的G表示协程用大写的M表示线程。 问题 锁竞争每个M线程想要执行、放回G协程都必须访问一个全局G队列因此对G的访问需要加锁以保证并发安全。当有很多线程时锁竞争激烈影响系统性能。局部性破坏M转移G会造成延迟和额外的系统负载。例如当一个G内创建另一个G’时为了继续执行G需要将G’交给另一个M’执行这会破坏程序的局部性。系统开销CPU在线程之间频繁切换导致频繁的系统调用增加了系统开销。
GMP模型的设计思想
为了克服上述问题Go引入了GMP模型 基本概念 Go语言使用GMP模型来管理并发执行。GMP模型由三个核心组件组成GGoroutine、MMachine、PProcessor。 GGoroutine Goroutine是Go语言中的协程代表一个独立的执行单元。Goroutine比线程更加轻量级启动一个Goroutine的开销非常小。Goroutine的调度由Go运行时在用户态进行。 MMachine M代表操作系统的线程。M负责实际执行Go代码。一个M可以执行多个Goroutine但同一时间只能执行一个Goroutine。M与操作系统的线程直接对应Go运行时通过M来利用多核CPU的并行计算能力。 PProcessor P代表执行上下文Processor。P管理着可运行的Goroutine队列并负责与M进行绑定。P的数量决定了可以并行执行的Goroutine的数量。Go运行时会根据系统的CPU核数设置P的数量。 GMP模型的组成 全局G队列存放等待运行的G。P的本地G队列存放不超过256个G。当新建协程时优先将G存放到本地队列本地队列满了后将一半的G移动到全局队列。M内核态线程线程想要运行协程需要先获取一个P从P的本地G队列中获取G。当本地队列为空时会尝试从全局队列或其他P的本地G列表中偷取G。P列表程序启动时创建GOMAXPROCS个P并保存在数组中。 调度器与OS调度器结合Go的Goroutine调度器与操作系统调度器结合OS调度器负责将线程分配给CPU执行。 设计策略
复用线程的两个策略 Work Stealing机制当本线程没有可执行的G时优先从全局G队列中获取一批G。如果全局队列中没有则尝试从其他P的G队列中偷取G。Hand Off机制当本线程因G进行系统调用等阻塞时线程会释放绑定的P把P转移给其他空闲的M执行。 利用并行有GOMAXPROCS个P则可以有同样数量的线程并行执行。抢占式调度Goroutine是协作式的一个协程只有让出CPU才能让下一个协程执行而Goroutine执行超过10ms就会强制让出CPU防止其他协程饿死。特殊的G0和M0 G0每次启动一个M都会创建的第一个Goroutine仅用于调度不指向任何可执行的函数。每个M都有一个自己的G0在调度或系统调用时使用G0的栈空间。M0启动程序后的第一个主线程负责执行初始化操作和启动第一个Goroutine此后与其他M一样。
调度策略 创建两步 通过go func()创建一个协程。新创建的协程优先保存在P的本地G队列如果本地队列满了会将P本地队列中的一半G打乱顺序移入全局队列。 唤醒获取 创建G时运行的G会尝试唤醒其他的PM组合去执行。假设G2唤醒了M2M2绑定了P2但P2本地队列没有G此时M2为自旋线程。M2便会尝试从全局队列中获取G。 偷取 假设P的本地队列和全局队列都空了会从其他P偷取一半G到自己的本地队列执行。 切换逻辑 G1运行完后M上运行的协程切换回G0G0负责调度时协程的切换。先从P的本地队列获取G2从G0切换到G2从而实现M的复用。 自旋 自旋线程会占用CPU时间但创建销毁线程也会消耗CPU时间系统最多有GOMAXPROCS个自旋线程其余的线程会在休眠M队列里。 系统调用 当G进行系统调用时会进入内核态被阻塞GM会绑定在一起进行系统调用。M会释放绑定的P把P转移给其他空闲的M执行。当系统调用结束时GM会尝试获取一个空闲的P。 阻塞处理 当G因channel或network I/O阻塞时不会阻塞M当超过10ms时M会寻找其他可运行的G。 公平性 调度器每调度61次时会尝试从全局队列里取出待运行的Goroutine来运行如果没有找到就去其他P偷一些Goroutine来执行。
GMP模型的优势
高效的资源利用通过在用户态进行调度避免了频繁的上下文切换带来的开销充分利用CPU资源。轻量级并发Goroutine比线程更加轻量级可以启动大量的Goroutine而不会消耗大量内存。自动调度Go运行时自动管理Goroutine的调度无需程序员手动干预简化了并发编程的复杂度。
面试题
如果问到了说一说GMP调度模型建议需要说的内容
在面试中如果被问到GMP调度模型建议全面地回答以下内容。如果能完整且详细地讲述这些内容将会展示你对GMP调度模型的深刻理解和熟练掌握这将是面试中的亮点。
基本概念
线程的内核态和用户态 线程分为“内核态”和“用户态”。用户态线程即协程必须绑定一个内核态线程。CPU只负责处理内核态线程。 调度器 在Go中线程是运行Goroutine的实体。调度器的功能是将可运行的Goroutine分配到工作线程上。 映射关系 在Go语言中线程与协程的映射关系是多对多的。这样避免了多个协程对应一个线程时出现的无法使用多核和并发的问题。Go的协程是协作式的只有让出CPU资源才能调度。如果一个协程阻塞只有一个线程在运行其他协程也会被阻塞。
四个概念
全局队列 存放等待运行的Goroutine。 本地队列 每个P处理器都有一个本地队列存放不超过256个Goroutine。新建协程时优先放入本地队列本地队列满了则将一半的G移入全局队列。 GMP GGoroutineGo语言中的协程。MMachine内核态线程运行Goroutine的实体。PProcessor处理器包含运行Goroutine的资源和本地队列。
设计策略
复用线程
Stealing机制当一个线程没有可执行的G时会从全局队列或其他P的本地队列中偷取G来执行。Hand off机制当一个线程因G进行系统调用等阻塞时线程会释放绑定的P把P转移给其他空闲的M执行。
P并行
有GOMAXPROCS个P代表最多有这么多个线程并行执行。
抢占式调度
Goroutine执行超过10ms就会强制让出CPU防止其他协程饿死。
特殊的G0和M0
G0每个M启动时创建的第一个Goroutine仅用于调度不执行用户代码。每个M都有一个G0。M0程序启动后的第一个主线程负责初始化操作和启动第一个Goroutine。
调度策略
创建
通过go func()创建一个协程。新创建的协程优先保存在P的本地G队列如果本地队列满了会将P本地队列中的一半G移入全局队列。
唤醒
创建G时当前运行的G会尝试唤醒其他PM组合执行。若唤醒的M绑定的P本地队列为空M会尝试从全局队列获取G。
偷取
如果P的本地队列和全局队列都为空会从其他P偷取一半G到自己的本地队列执行。
切换
G1运行完后M上运行的Goroutine切换回G0G0负责调度协程的切换。G0从P的本地队列获取G2实现M的复用。
自旋
自旋线程会占用CPU时间但创建销毁线程也消耗CPU时间。系统最多有GOMAXPROCS个自旋线程其他线程在休眠M队列里。
系统调用
当G进行系统调用时进入内核态被阻塞M会释放绑定的P把P转移给其他空闲的M执行。当系统调用结束GM会尝试获取一个空闲的P。
阻塞处理
当G因channel或network I/O阻塞时不会阻塞M。超过10ms时M会寻找其他可运行的G。
公平性
调度器每调度61次时会尝试从全局队列中取出待运行的Goroutine来运行。如果没有找到就去其他P偷一些Goroutine来执行。
欢迎关注 ❤
我们搞了一个免费的面试真题共享群互通有无一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信wangzhongyang1993备注csdn面试群。