goroutine原理分析
<h2>进程和线程</h2>
在讲解goroutine之前,先来熟悉一下进程和线程的概念,因为只有通过概念之前对比,才能更加理解这些概念。 计算机的使用,大都是以进程为单位来管理的,比如我打开电脑版微信,桌面启动一个微信程序,本质上计算机启动了一个为微信进程,打开浏览器、播放器等等类似,当然有的应用软件不只启动一个进程。 在windows下,可以通过任务管理器看到启动的进程;在linux下,可以通过ps -aux命令,看到所有的进程。 由此,可以大致了解到,所谓进程就是运行的程序。 如果安装完QQ软件,你不运行QQ,那么QQ就是一堆静态的躺在硬盘上的文件,当你一旦点击运行,那么计算机便创建了一个QQ进程,这个时候,就可以输入账号和密码登录QQ了,这就是和QQ进程交互的过程。 到这里,可以知道,进程是CPU和内存有关系的,因为程序的运行需要CPU和内存,更专业的说法就是进程就是程序在内存中的镜像,因为只有把程序载入内存才能运行。 上面讲了那么多废话,无非就是引入进程的以下特征: 由于第一点资源上的独立,第二点也就是自然成立了。 由于进程从宏观的上就可以看到,所以容易理解,但是线程是比进程更小的单位,貌似就不容易那么理解了。世间万事都是如此,更微观的现象,了解的成本的就越高,了解的人就越少。首先,线程是由进程创建,并且可以创建多个线程,正确的说是,线程的创建、运行、销毁都是由进程控制。 第一点:进程的拥有的资源,所有线程的线程都可以”看见“,都可以访问到。 第二点:线程是CPU执行的基本单位。 一点不能理解,既然进程分配资源的基本单位,拥有资源,那么这些资源给谁用呢?就是线程,线程来使用这些资源,所有的线程都可以使用到。线程使用这些资源干嘛呢?干活。 这里需要补充一点,CPU每次只能执行一个线程,那么其他的线程只能排队等待,至于这个线程要”霸占“CPU多久,取决于系统的线程调度算法,一般是执行一定的时间就让出CPU,然后排队等待。 下面做一个简单的比喻: 进程就好比一个公司,拥有很多资源,包括办公室、电脑、食堂、班车等等,而且公司与公司之间相互独立,互不影响。那么线程就是公司里的员工,每天都要工作,每个人都是基本的人力单位,所有的员工都可以使用公司的资源。所以进程与进程的独立性很强,基本不受对方的影响。但是一个进程的所有线程,就不那么独立了,因为使用同一个进程的资源,有时候就产生”矛盾“了,你看一个公司的所有员工之前经常发生摩擦和争吵。 刚才说了,一个进程拥有多个线程,就好比一家公司有好多员工,每个员工的工作任务不同,有的人写代码、有的人设计UI、有的人负责运营、有的人负责人事,他们的工作基本上是并行的。我之所以说基本上是并行的,是因为有时候他们之间也需要相互等待,比如软件没有开发完,就不能让测试进行测试,更不能发布到线上。 整体而言,一个公司的人越多,做事情的速度越快。但是不是全然正确,因为《人月神话》,因为一件工作的粒度不能无限细分下去,举个极端的例子,一车砖头,10个人1小时搬完,请问10万个人搬需要多少小时?是1万分之小时吗?也就是0.36秒?算了吧,10万个人排队就超过1小时,这时候人多反而降低效率了。当然,我举的这个例子很极端,只是为了说明问题。 之前做过这样一个需求:有一个目录下会生成大量的文件,需要及时转移到另外一个目录下,文件的大小2KB—2GB之间,我最开始的做法是配置多个线程来转移文件,因为多个线程读取同一个目录,所以必须采用互斥锁。 线程的具体做法是: 经过测试就发现一个问题,假如这某一个时候,生成的文件都比较大,都是2GB,那么文件大,文件的数量就少,只能少量线程可以搬运文件,有的线程都空闲着,根本无法发挥多线程的优势。 另一种情况时,生产的文件都很小,只有1KB那么大,但是生成的速度很快,几秒钟便可以生成近百万个文件,如果这个时候,每个线程每次只获取50个文件,那么线程就需要多次访问互斥锁,性能反而降低,怎么办?改进方法:每个线程每次获取1万个文件,性能可以提高不少。 这个案例就涉及到任务粒度划分的问题,第一种情况,每个文件2G,任务粒度很大,一个文件只能由一个线程来处理,多线程没啥优势。第二种情况,每个文件1KB,任务粒度太小,每个线程根本没有饱和,造成线程争夺资源损耗性能。 总结: 协程,是一个线程更小的单位,由线程创建,由于线程是CPU调度执行的最小单元,那么第一个结论就是: 一个线程里协程,是不能并发的,因此协程之间不用加锁。 一个人每天早上上班后,开始投入工作,认真写代码,此时主协程在工作,代码写了250行,突然上级让TA过去开会,这时候放下手头的工作,创建一个开会的协程,开始进入会议模式,记下100行会议纪要,突然TA的电话响了,停止会议,创建接电话的协程,进入接电话模式,5分钟后电话结束,接电话的协程结束,回到刚才的切换的协程,即开会的协程,然后继续接着100行会议纪要继续记录,等会议结束,会议协程结束,回到刚才切换的协程,即写代码的协程,继续接着250行代码继续写。 以上的过程,大概就是协程切换的过程,是一个人串行的干多件事,干完一件事,就回到上一件事继续接着干。 其实协程的切换就是函数栈帧的切换,以为线程的结构就是栈帧、程序指针、各种寄存器等等,CPU拿到这些东西就可以执行一个线程了。 思考: 一个线程应该至少包含哪些东西? CPU是如何通过机器指令执行程序的。所谓的栈,只是空间,但是在机器指令中,都是地址,变量、函数都是地址。 最近学习和使用golang已经有半年多了,对于一个C/C 程序员来说,golang只是一门语言而已,并没有什么神奇之处,正如侯捷所说: 代码面前,了无秘密。 一门编程语言,最终还是要依赖操作系统实现各种功能,你看golang对应每个操作系统,都有一个版本,windows的话,你就需要下载windows版本的golang ,linux系统就要下载linux版本的golang。 说到这里,就不得不说golang的一个重要功能,就是协程——goroutine,利用关键go就可以轻易的开启协程,编写并发程序,利用chan就可以实现协程之间的通信。 上面,我们说了,真正的协程是不能并发的,因为一个协程在线程内部,而线程又是CPU执行的最小单位。但是goroutine是天生可以并发的,在语言层面获得支持,所以golang的强大,其实是golang的运行时干了太多的事情。 编译运行,查看这个进程的线程数量,执行: top -H -p <code>pidof hello</code> 结果发现,一个简单的hello.go运行后,竟然开启了5个线程(包括主线程),那么goroutine之所以能轻松并发,是这些线程的支持,这些线程来执行应用层goroutine的任务。 由上面可知,go的运行时启动多个线程来执行多个goroutine任务,最开始go的调度器是GM模型。 G:表示goroutine,应用层开启的任务。 M:表示golang运行时开启的线程(machine),刚才在我的机器看到的是5个hello线程,当然这些线程的个数和CPU的核数,以及当前的goroutine的数量有关系,和当前的goroutine的行为有一定的关系。 这样的话,多个G相当于是任务队列,多个M构成线程池,然后每个M取出一定量的任务来执行,看起来很完美,但是实际中存在一些问题。 于是在G和M之间引入P, P:是M调度G的一个中间层,可以理解为是对CPU的抽象,因为它的个数是由CPU的核数确定,可以由runtime.GOMAXPROCS(num)指定,程序运行后不会再改变。 G要想到M上执行,必须先绑定一个P,然后P在M上执行,所以我说P是G和M的中间层,P的数量决定了,同时最多有几个G在执行,P数数量小于等于CPU的核数。P可以控制整个程序的并发程度。 由P来完成一部分M的任务,之前是M从任务队列取任务,现在是P从任务对列取任务,放到自己的本地队列,当M上执行的G阻塞时,P与M分离,这个阻塞的G仍然和M绑在一起继续阻塞等待系统调用返回。那么P就可以继续和其他的M结合,你看M和G就解耦了,解决了GM模型存在的第二和第三个问题。此时,M只执行任务,P只分发任务,解耦了之前的M执行任务,又要管理任务的耦合。 这时候,M面对的不是G了,M只需找到一个P去结合,然后执行P中的G。 测试结果如下:我的机器8核,64位系统,golang 1.11 这个进程CPU占有率640%,一共启动34个线程,加上主线程就是35个,业务层代码启动了30个协程。 CPU占有率很少,因为斗都在休眠中,后台线程12个,加上主线程一共13个,这个程序也是启动30个协程。 但是30个协程任务执行完后,全部退出,主协程休眠中,但是底层34的个线程依然还在,没有销毁。 加上上面的hello.go,一共也就三个测试程序,按理说,我是得不到什么结论的,但是阅读了其他资料,在上自己的推论,可得到一下我自己的结论。 1、执行线程的数量是不定的,根据需要创建,我的机器上最少是6个。 2、协程越多,执行线程未必越多,取决于于协程是否忙碌,忙碌的协程越多,执行线程就越多。 3、执行线程根据任务繁忙程度来创建,任务执行完,这些线程依然还在,没有销毁。 您可能感兴趣的文章:
Goroutine的调度分析(一)
golang 深入浅出之 goroutine 理解
GO 语言之 Goroutine 原理解析
golang 切片截取 内存泄露_怎么看待Goroutine 泄露
goroutine 调度器
golang goroutine 通知_深入golang之---goroutine并发控制与通信
goroutine泄露:原理、场景、检测和防范
Goroutine是如何工作的
[Go 教程系列笔记] goroutine(协程)
Goroutine并发调度模型深度解析&手撸一个协程池