|
从操作系统的角度理解Goroutine – Go 协程设计系列(1/2)
从操作系统的角度理解Goroutine – Go 协程设计系列(1/2)
许狄欢@贝壳找房
贝壳产品技术
贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容
2021年04月16日 11:52
1 开篇Hello Gopher!想必大家也知道,得益于Go语言天生的并发特性,很多工程师一脚跨入了并发编程的大门。Go底层的协程机制 ,其效率之高,使得很多Web应用仿佛不费吹灰之力,并发能力就翻了一番,表现喜人。但,Gopher们虽然迈进了并发编程的大门,却发现自己还是偶尔会被并发的门槛绊倒。有时候线上内存泄露导致机器宕机,有时候共享内存导致数据不一致,有时候死锁出现得百思不得其解…做技术活需要有匠心,知其然知其所以然,Gopher要问问自己三个问题:什么是并发?Go语言为何要实现goroutineGo语言如何进行并发?这系列的文章共二篇,(一)从操作系统OS Scheduler的角度,理解前两个问题(二)深入探究Go Scheduler的机制,理解GO如何实现并发本篇为第一篇,结构如下:进程与线程;线程的并发和调度;协程和Goroutine;结语2 进程与线程操作系统是一个精密,复杂又庞大的软件,管理着软件,也管理着硬件。理解操作系统,永远都绕不过两个概念:进程,线程。进程:进程是程序运行的过程,拥有独立的空间,持有系统分配的资源。线程:线程是比进程更轻量的调度单位,进程持有线程,操作系统调度着线程的运行。光看概念比较晦涩,下面提供一张假想图:当手机上的微信程序处于运行中,主进程持有了若干的线程,每个线程有专门作业的处理逻辑。那么,计算机(手机也是计算机)是如何同时做多件事呢?为了明白这个问题,得先了解计算机如何做一件事。每个程序最终都是一系列需要按顺序执行的机器指令集合。你写下的一段代码,最终会被翻译成如下的样子:操作系统通过线程,按序执行这些指令。每个程序在开始运行时,操作系统会为其创建一个进程,这个被创建的进程,会持有一个主线程,用来运行程序。一个进程可以持有多个线程。进程更多是持有计算机的内存资源,线程则持有计算资源,也就是CPU的计算能力。线程的运行是彼此独立的,轮流在CPU内核上运行。上图的三个线程ABC,都是独立运行,轮流在CPU上跑,当线程A在CPU上执行的时候,用户的消息才可以被发送出去。3 线程的并发执行理解了计算机怎么做一件事,那么计算机是如何同时做多件事的?接下来讨论的内容全部基于单核的计算器,现在的机器大都是多核,确实会同时执行不同的程序,但为了简化问题,我们就限定在单核。其实,计算机“同时做多件事“,是个假象。上边提到,线程ABC在CPU上轮流执行。不仅如此,任一时刻,一个CPU上只有一个线程在运行。我们都知道电影之所以是“动态的“,是因为它的帧率高于人眼能感知的范围。类似的, 计算机切换在CPU上执行线程的频率,远远高于人的感知范围,所以用户感觉计算机在“同时做多件事“,看起来就像下图:那么,计算机是按什么规则去切换线程呢?怎么切呢?这就涉及到线程的状态,和操作系统对线程的调度 OS Scheduler的工作。线程有三个基本的状态:Waiting(阻塞):线程被暂停运行,在等待某个资源准备完毕。举个例子:线程在等待网络请求的响应,会进入阻塞状态;Runnable(就绪):线程准备就绪,需要OS Scheduler分配时间片,好让自己能在Core上运行一会。举个例子:网络响应的事件到了,等待这个事件的线程可以进入就绪状态;Executing(执行):线程正在CPU上爽快执行。线程简化的状态机:4 OS SchedulerOS Scheduler的工作:当一个线程要在CPU上执行时,OS Scheduler 会给它分配限额的时间片。时间到了,线程就会被OS Scheduler让出资源,没得商量。OS Scheduler基于上面介绍的几个状态,决策是否需要把某个线程的计算资源让出来,切换给另外一个线程执行。比方说,线程阻塞时,需要让出CPU资源,替换别的线程所以线程,OS Scheduler和CPU的关系模型如下:线程的调度决策,涉及大量硬核算法,无法一文介绍。计算机领域的先驱用了几十年的时间,给这个问题找到了成熟的解法。在高级应用的角度,开发者只需要知道,只要CPU上的线程阻塞了,或者时间用完了,OS Scheduler就会从CPU上让出当前线程(还有其他让出条件,如中断等,此处省略)。OS Scheduler解决了程序的并发问题,也提高了CPU的利用率:系统可以通过切换线程,并发执行多个程序;CPU的计算资源的利用率提升了;5 上下文切换OS Scheduler对线程进行的切换,被称为“上下文切换”。上面的线程轮流运行图,在切换的间隙我留了一些黄色空间,其实是上下文切换所需的时间成本。上下文切换会做什么呢?简单来说包括:保存当前线程A的堆栈数据保存线程A下一个要执行指令的地址将线程A的资源让出恢复要上位的线程B之前的运行现场给线程B分配计算资源…这系列的操作,称为上下文切换(Context Switch)。上下文切换需要执行这么多的工作,可见其有时间成本。时间对于CPU来说,就是资源。机器存在差异,一般而言,上下文切换需要1000+纳秒,这个时间大概可以执行10K+条指令。可见每一次切换都意味着计算资源的浪费。6 协程到此简要总结一下上文:线程是操作系统进行调度的基本单位操作系统通过上下文切换实现并发上下文切换有时间成本上文提到,当线程发生阻塞时,OS Scheduler会让出线程的资源。举个例子:假如线程A是一个微信发消息的线程,它的主要工作内容就是不断发送网络请求,也就是要等待网络请求的响应,会进入阻塞,这时候就算分配的时间没用完,也会被让出资源。操作系统让出线程的时候可不管线程是哪个进程的,有可能它就按优先级,把微信发消息的线程让出,切换拼多多拉取商品列表的线程。好嘛,从微信进程(进程)角度来说:我的子任务“发消息“要等待,那操作系统好歹让我干点别的,拉取一下朋友圈也行呀,怎么直接就把我切下来了呢?我时间还没用完呢…为了优化上面这两个问题:从程序角度,线程能尽量用满时间片;从CPU角度,减少上下文切换;出现了用户态线程。操作系统的空间分用户态和内核态,因此,线程也分两大类:内核态线程:运行在内核态空间,具有计算资源,操作硬件等高权限;用户态线程:运行在用户态空间,其执行需要依赖内核态线程,占用资源比内核态线程少很多;用大白话说:能在CPU上跑的都是内核态线程,用户态线程就是应用程序自己内部搞的鬼,CPU根本不知道也不关心。用专业的术语描述:OS Scheduler调度的,是内核态线程;用户态线程对操作系统来说,只是一些数据,并非可调度的对象;用户态线程需要交由内核线程执行;由于不持有真正执行所需的资源,用户态线程本身的上下文切换成本降低很多;用户态线程的调度,由用户空间的应用程序去实现;协程(Coroutine),就是用户态线程的一种实现。协程在Wikipedia的部分解释:Coroutines are very similar to threads. However, coroutines are cooperatively multitasked, whereas threads are typically preemptively multitasked.协程非常类似于线程。然而,协程是协同多任务的,而线程通常是抢占多任务的。(机翻质量甚好。)协程起到什么作用呢?其实,解释中的“协同”说明了设计的初衷。还是举微信的例子:假如用协程来运行发消息,朋友圈和支付,并将它们都绑定在一个内核线程上,并且在用户程序做了协程的协同切换,则: CPU认为该线程一直在执行,允许其跑满分配的时间片;用协程来运行的好处不言而喻:减少了CPU进行上下文切换的频率。其实,实现了协程的编程语言很多,下面是Wikipedia的介绍,有的是内部实现,有的是给使用者提供了API:不同语言实现的协程和内核线程的关系,有如下几种:N对1优点:协程的上下文切换快缺点:无法利用CPU多核的计算能力N对N优点:专属线程,充分利用CPU的计算能力缺点:线程数量增多之后,线程上下文切换频繁N对M(N可以大于M) (也是GO语言选择的方案)优点:权衡了CPU的多核计算能力和协程的上下文切换缺点:调度策略复杂 ,需要解决饥饿,优先级权重等问题7 GoroutineGo语言实现的Goroutine,是用户态线程,也是协程,它的取名也来自Coroutine (协程)。Go设计者开发了Goroutine的调度器——Go Scheduler。包含了三个主要的概念:G:Goroutine,用户态线程(协程)M:Machine,内核线程P:Processor,Go定义的处理器,能调度GoroutineGMP的关系类似内核线程,CPU和OS Scheduler的关系:G需要在M上运行,且被P管理调度。如果从全局的角度再看一次,则如图:8 结语和下一篇的内容相信经过这篇文章的介绍类比,Gopher多多少少加深了对Goroutine,乃至线程的一些理解。这时候可能会存在一些疑问:Go Scheduler的调度原理和OS Scheduler类似吗?Goroutine也有类似线程的若干个状态吗?Goroutine是如何排队等待调度的?Gorouinte的生命周期又是如何?这些疑问,将在下一篇文章讲述,也是介绍GO协程设计的第二篇文章,结构如下:Goroutine调度机制;Go Gc和Goroutine调度之间的关系;常见的并发风险和解决方案;希望这篇文章有所帮助,不对的地方请指出。
预览时标签不可点
GO6后端27GO · 目录#GO上一篇gin框架之路由前缀树初始化分析下一篇贝壳Go实现的多云对接存储网关建设关闭更多小程序广告搜索「undefined」网络结果
|
|