返璞归真、回归实质,这些技能特色背后的底层事理到底是什么?如何能普通易懂、绝不费力真正透彻理解这些技能背后的事理,正是《从根上理解高性能、高并发》系列文章所要分享的。
1.2 文章源起我整理了相称多有关IM、推送等即时通讯技能干系的资源和文章,从最开始的开源IM框架MobileIMSDK,到网络编程经典巨著《TCP/IP详解》的在线版本,再到IM开拓纲领性文章《新手入门一篇就够:从零开拓移动端IM》,以及网络编程由浅到深的《网络编程
越往知识的深处走,越以为对即时通讯技能理解的太少。于是后来,为了让开发者门更好地从根本电信技能的角度理解网络(尤其移动网络)特性,我跨专业网络整理了《IM开拓者的零根本通信技能入门》系列高阶文章。这系列文章已然是普通即时通讯开拓者的网络通信技能知识边界,加上之前这些网络编程资料,办理网络通信方面的知识盲点基本够用了。
对付即时通讯IM这种系统的开拓来说,网络通信知识确实非常主要,但回归到技能实质,实现网络通信本身的这些技能特色:包括上面提到的线程池、零拷贝、多路复用、事宜驱动等等,它们的实质是什么?底层事理又是若何?这便是整理本系列文章的目的,希望对你有用。

《从根上理解高性能、高并发(一):深入打算机底层,理解线程与线程池》
《从根上理解高性能、高并发(二):深入操作系统,理解I/O与零拷贝技能》
《从根上理解高性能、高并发(三):深入操作系统,彻底理解I/O多路复用》
《从根上理解高性能、高并发(四):深入操作系统,彻底理解同步与异步》
《从根上理解高性能、高并发(五):深入操作系统,理解高并发中的协程》( 本文)
《从根上理解高性能、高并发(六):高并发高性能做事器到底是如何实现的 (稍后发布..)》
1.4 本篇概述接上篇《深入操作系统,彻底理解同步与异步》,本篇是高性能、高并发系列的第5篇文章。
协程是高性能高并发编程中不可或缺的技能,包括即时通讯(IM系统)在内的互联网产品运用产品中运用广泛,比如号称支撑微信海量用户的后台框架便是基于协程打造的(详见《开源libco库:单机千万连接、支撑微信8亿用户的后台框架基石》)。而且越来越多的当代编程措辞都将协程视为最主要的措辞技能特色,已知的包括:Go、Python、Kotlin等。
因此理解和节制协程技能对付很多程序员(尤其海量网络通信运用的后端程序员)来说是相称有必要的,本文正是为你解惑协程技能事理而写。
2、本文作者应作者哀求,不供应真名,也不供应个人照片。
本文作者紧张技能方向为互联网后端、高并发高性能做事器、检索引擎技能,网名是“码农的荒岛求生”,"大众号“码农的荒岛求生”。感谢作者的无私分享。
3、正文弁言作为程序员,想必你多多少少听过协程这个词,这项技能近年来越来越多的涌如今程序员的视野当中,尤其高性能高并发领域。当你的同学、同事提到协程时如果你的大脑一片空缺,对其毫无观点。。。
那么这篇文章正是为你量身打造的。
话不多说,本日的主题便是作为程序员,你该当如何彻底理解协程。
4、普通的函数我们先来看一个普通的函数,这个函数非常大略:
def func():
print("a")
print("b")
print("c")
这是一个大略的普通函数,当我们调用这个函数时会发生什么?
1)调用func;2)func开始实行,直到return;3)func实行完成,返回函数A。是不是很大略,函数func实行直到返回,并打印出:
a
b
c
So easy,有没有,有没有!
很好!
把稳这段代码是用python写的,但本篇关于协程的谈论适用于任何一门措辞,由于协程并不是某种措辞特有的。而我们只不过恰好利用了python来用作示例,因其足够大略。
那么协程是什么呢?
5、从普通函数到协程接下来,我们就要从普通函数过渡到协程了。和普通函数只有一个返回点不同,协程可以有多个返回点。
这是什么意思呢?
void func() {
print("a")
停息并返回
print("b")
停息并返回
print("c")
}
普通函数下,只有当实行完print("c")这句话后函数才会返回,但是在协程下当实行完print("a")后func就会因“停息并返回”这段代码返回到调用函数。
有的同学可能会一脸懵逼,这有什么神奇的吗?
我写一个return也能返回,就像这样:
void func() {
print("a")
return
print("b")
停息并返回
print("c")
}
直接写一个return语句确实也能返回,但这样写的话return后面的代码都不会被实行到了。
协程之以是神奇就神奇在当我们从协程返回后还能连续调用该协程,并且是从该协程的上一个返回点后连续实行。
就好比孙悟空说一声“定”,函数就被停息了:
void func() {
print("a")
定
print("b")
定
print("c")
}
这时我们就可以返回到调用函数,当调用函数什么时候想起该协程后可以再次调用该协程,该协程会从上一个返回点连续实行。
Amazing,有没有,集中把稳力,千万不要翻车。
只不过孙大圣利用的口诀“定”字,在编程措辞中一样平常叫做yield(其它措辞中可能会有不同的实现,但实质都是一样的)。
须要把稳的是:当普通函数返回后,进程的地址空间中不会再保存该函数运行时的任何信息,而协程返回后,函数的运行时信息是须要保存下来的。
接下来,我们就用实际的代码看一看协程。
6、“Talk is cheap,show me the code”下面我们利用一个真实的例子来讲解,措辞采取python,不熟习的同学不用担心,这里不会有理解上的门槛。
在python措辞中,这个“定”字同样利用关键词yield。
这样我们的func函数就变成了:
void func() {
print("a")
yield
print("b")
yield
print("c")
}
把稳:这时我们的func就不再是简大略单的函数了,而是升级成为了协程,那么我们该怎么利用呢?
很大略:
def A():
co =func() # 得到该协程
next(co) # 调用协程
print("in function A") # do something
next(co) # 再次调用该协程
我们看到虽然func函数没有return语句,也便是说虽然没有返回任何值,但是我们依然可以写co = func()这样的代码,意思是说co便是我们拿到的协程了。
接下来我们调用该协程,利用next(co),运行函数A看看实行到第3行的结果是什么:
a
显然,和我们的预期一样,协程func在print("a")后因实行yield而停息并返回函数A。
接下来是第4行,这个毫无疑问,A函数在做一些自己的事情,因此会打印:
a
in function A
接下来是重点的一行,当实行第5行再次调用协程时该打印什么呢?
如果func是普通函数,那么会实行func的第一行代码,也便是打印a。
但func不是普通函数,而是协程,我们之前说过,协程会在上一个返回点连续运行,因此这里该当实行的是func函数第一个yield之后的代码,也便是 print("b")。
a
in function A
b
看到了吧,协程是一个很神奇的函数,它会自己记住之前的实行状态,当再次调用时会从上一次的返回点连续实行。
7、图形化阐明为了让你更加彻底的理解协程,我们利用图形化的办法再看一遍。
首先是普通的函数调用:
在该图中:方框内表示该函数的指令序列,如果该函数不调用任何其它函数,那么该当从上到下依次实行,但函数中可以调用其它函数,因此其实行并不是大略的从上到下,箭头线表示实行流的方向。
从上图中我们可以看到:我们首先来到funcA函数,实行一段韶光后创造调用了另一个函数funcB,这时掌握转移到该函数,实行完成后回到main函数的调用点连续实行。这是普通的函数调用。
接下来是协程:
在这里:我们依然首先在funcA函数中实行,运行一段韶光后调用协程,协程开始实行,直到第一个挂出发点,此后就像普通函数一样返回funcA函数,funcA函数实行一些代码后再次调用该协程。
把稳:协程这时就和普通函数不一样了,协程并不是从第一条指令开始实行而是从上一次的挂出发点开始实行,实行一段韶光后碰着第二个挂出发点,这时协程再次像普通函数一样返回funcA函数,funcA函数实行一段韶光后全体程序结束。
8、函数只是协程的一种特例
怎么样,神奇不神奇。和普通函数不同的是,协程能知道自己上一次实行到了哪里。
现在你该当明白了吧,协程会在函数被停息运行时保存函数的运行状态,并可以从保存的状态中规复并连续运行。
很熟习的味道有没有,这不便是操作系统对线程的调度嘛(见《深入打算机底层,理解线程与线程池》),线程也可以被停息,操作系统保存线程运行状态然后去调度其它线程,此后该线程再次被分配CPU时还可以连续运行,就像没有被停息过一样。
只不过线程的调度是操作系统实现的,这些对程序员都不可见,而协程是在用户态实现的,对程序员可见。
这便是为什么有的人说可以把协程理解为用户态线程的缘故原由。
此处该当有掌声。
也便是说现在程序员可以扮演操作系统的角色了,你可以自己掌握协程在什么时候运行,什么时候停息,也便是说协程的调度权在你自己手上。
在协程这件事儿上,调度你说了算。
当你在协程中写下 yield 的时候便是想要停息该协程,当利用 next() 时便是要再次运行该协程。
现在你该当理解为什么说函数只是协程的一种特例了吧,函数实在只是没有挂出发点的协程而已。
9、协程的历史有的同学可能认为协程是一种比较新的技能,然而实在协程这种观点早在1958年就已经提出来了,要知道这时线程的观点都还没有提出来。
到了1972年,终于有编程措辞实现了这个观点,这两门编程措辞便是Simula 67 以及Scheme。
但协程这个观点始终没有盛行起来,乃至在1993年还有人考古一样专门写论文挖出协程这种古老的技能。
由于这一期间还没有线程,如果你想在操作系统写出并发程序那么你将不得不该用类似协程这样的技能,后来线程开始涌现,操作系统终于开始原生支持程序的并发实行,就这样,协程逐渐淡出了程序员的视线。
直到近些年,随着互联网的发展,尤其是移动互联网时期的到来,做事端对高并发的哀求越来越高,协程再一次重回技能主流,各大编程措辞都已经支持或操持开始支持协程。
那么协程到底是如何实现的呢?
10、协程到底是如何实现的?让我们从问题的实质出发来思考这个问题:协程的实质是什么呢?
实在便是可以被停息以及可以被规复运行的函数。那么可以被停息以及可以被规复意味着什么呢?
看过篮球比赛的同学想必都知道(没看过的也能知道),篮球比赛也是可以被随时停息的,停息时大家须要记住球在哪一方,各自的站位是什么,等到比赛连续的时候大家回到各自的位置,裁判哨子一响比赛连续,就像比赛没有被停息过一样。
看到问题的关键了吗:比赛之以是可以被停息也可以连续是由于比赛状态被记录下来了(站位、球在哪一方),这里的状态便是打算机科学中常说的高下文(context)。
回到协程。
协程之以是可以被停息也可以连续,那么一定要记录下被停息时的状态,也便是高下文,当连续运行的时候要规复其高下文(状态)其余:函数运行时所有的状态信息都位于函数运行时栈中。
函数运行时栈便是我们须要保存的状态,也便是所谓的高下文。
如图所示:
从上图中我们可以看出:该进程中只有一个线程,栈区中有四个栈帧,main函数调用A函数,A函数调用B函数,B函数调用C函数,当C函数在运行时全体进程的状态就如图所示。
现在:我们已经知道了函数的运行时状态就保存在栈区的栈帧中,接下来重点来了哦。
既然函数的运行时状态保存在栈区的栈帧中,那么如果我们想停息协程的运行就必须保存全体栈帧的数据,那么我们该将全体栈帧中的数据保存在哪里呢?
想一想这个问题:全体进程的内存区中哪一块是专门用来永劫光(进程生命周期)存储数据的?是不是大脑又一片空缺了?
先别空缺!
很显然:这便是堆区啊(heap),我们可以将栈帧保存在堆区中,那么我们该怎么在堆区中保存数据呢?希望你还没有晕,在堆区中开辟空间便是我们常用的C措辞中的malloc或者C++中的new。
我们须要做的便是:在堆区中申请一段空间,让后把协程的全体栈区保存下,当须要规复协程的运行时再从堆区中copy出来规复函数运行时状态。
再仔细想一想,为什么我们要这么麻烦的来回copy数据呢?
实际上:我们须要做的是直接把协程的运行须要的栈帧空间直接开辟在堆区中,这样都不用来回copy数据了,如下图所示。
从上图中我们可以看到:该程序中开启了两个协程,这两个协程的栈区都是在堆上分配的,这样我们就可以随时中断或者规复协程的实行了。
有的同学可能会问,那么进程地址空间最上层的栈区现在的浸染是什么呢?
答案是:这一区域依然是用来保存函数栈帧的,只不过这些函数并不是运行在协程而是普通线程中的。
现在你该当看到了吧,在上图中实际上共有3个实行流:
1)一个普通线程;2)两个协程。虽然有3个实行流但我们创建了几个线程呢?
答案是:一个线程。
现在你该当明白为什么要利用协程了吧:利用协程理论上我们可以开启无数并发实行流,只要堆区空间足够,同时还没有创建线程的开销,所有协程的调度、切换都发生在用户态,这便是为什么协程也被称浸染户态线程的缘故原由所在。
掌声在哪里?
因此:纵然你创建了N多协程,但在操作系统看来依然只有一个线程,也便是说协程对操作系统来说是不可见的。
这大概是为什么协程这个观点比线程提出的要早的缘故原由,可能是写普通运用的程序员比写操作系统的程序员最先碰着须要多个并行流的需求,那时可能都还没有操作系统的观点,或者操作系统没有并行这种需求,以是非操作系统程序员只能自己动手实现实行流,也便是协程。
现在你该当对协程有一个清晰的认知了吧。
11、协程技能观点小结
正文内容用了较多调侃语气,目的是希望能轻松诙谐地助你理解协程技能观点。那么,我们从严明专业知识来小结一下,到底什么是协程呢?
11.1 协程是比线程更小的实行单元协程是比线程更小的一种实行单元,你可以认为是轻量级的线程。
之以是说轻:个中一方面的缘故原由是协程所持有的栈比线程要小很多,java当中会为每个线程分配1M旁边的栈空间,而协程可能只有几十或者几百K,栈紧张用来保存函数参数、局部变量和返回地址等信息。
我们知道:而线程的调度是在操作系统中进行的,而协程调度则是在用户空间进行的,是开拓职员通过调用系统底层的实行高下文干系api来完成的。有些措辞,比如nodejs、go在措辞层面支持了协程,而有些措辞,比如C,须要利用第三方库才可以拥有协程的能力(比如微信开源的Libco库便是这样的,见:《开源libco库:单机千万连接、支撑微信8亿用户的后台框架基石》)。
由于线程是操作系统的最小实行单元,因此也可以得出,协程是基于线程实现的,协程的创建、切换、销毁都是在某个线程中来进行的。
利用协程是由于线程的切换本钱比较高,而协程在这方面很有上风。
11.2 协程的切换到底为什么很廉价?关于这个问题,我们回顾一下线程切换的过程:
1)线程在进行切换的时候,须要将CPU中的寄存器的信息存储起来,然后读入其余一个线程的数据,这个会花费一些韶光;2)CPU的高速缓存中的数据,也可能失落效,须要重新加载;3)线程的切换会涉及到用户模式到内核模式的切换,听说每次模式切换都须要实行上千条指令,很耗时。实际上协程的切换之以是快的缘故原由我认为紧张是:
1)在切换的时候,寄存器须要保存和加载的数据量比较小;2)高速缓存可以有效利用;3)没有用户模式到内核模式的切换操作;4)更有效率的调度,由于协程是非抢占式的,前一个协程实行完毕或者堵塞,才会让出CPU,而线程则一样平常利用了韶光片的算法,会进行很多没有必要的切换(为了只管即便让用户感知不到某个线程卡)。12、写在末了写到这里,相信你已经理解协程到底是怎么一回事了,关于协程更系统的知识可以自行查阅干系资料,我就不再啰嗦了。
下一篇《从根上理解高性能、高并发(六):高并发高性能做事器到底是如何实现的》,敬请期待!
《高性能网络编程(一):单台做事器并发TCP连接数到底可以有多少》
《高性能网络编程(二):上一个10年,著名的C10K并发连接问题》
《高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了》
《高性能网络编程(四):从C10K到C10M高性能网络运用的理论探索》
《高性能网络编程(五):一文读懂高性能网络编程中的I/O模型》
《高性能网络编程(六):一文读懂高性能网络编程中的线程模型》
《高性能网络编程(七):到底什么是高并发?一文即懂!
》
《以网游做事真个网络接入层设计为例,理解实时通信的技能寻衅》
《知乎技能分享:知乎千万级并发的高性能长连接网关技能实践》
《淘宝技能分享:手淘亿级移动端接入层网关的技能演进之路》
《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》
《一套原创分布式即时通讯(IM)系统理论架构方案》
《微信后台基于韶光序的海量数据冷热分级架构设计实践》
《微信技能总监谈架构:微信之道——大道至简(演讲全文)》
《如何解读《微信技能总监谈架构:微信之道——大道至简》》
《快速裂变:见证微信强大后台架构从0到1的演进进程(一)》
《17年的实践:腾讯海量产品的技能方法论》
《腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面》
《以微博类运用处景为例,总结海量社交系统的架构设计步骤》
《新手入门:零根本理解大型分布式架构的演进历史、技能事理、最佳实践》
《重新手到架构师,一篇就够:从100到1000万高并发的架构演进之路》
本文已同步发布于“即时通讯技能圈”"大众年夜众号。
同步发布链接是:http://www.52im.net/thread-3306-1-1.html