当前位置: 首页 > 科技观察

程序员应该如何理解高并发下的协程

时间:2023-03-17 21:30:37 科技观察

作为程序员,你一定或多或少听过协程这个词。这项技术近年来越来越多地出现在程序员的视野中,尤其是高性能并发领域。当你的同学和同事提到协程的时候,如果你的大脑一片空白,你根本就没有概念。..那么这篇文章就是为你准备的。话不多说,今天的话题就是作为程序员的你应该如何吃透协程。普通函数先来看一个普通函数,这个函数很简单:deffunc():print("a")print("b")print("c")这是一个简单的普通函数,当我们调用时会发生什么什么时候调用这个函数?调用funcfunc开始执行,直到returnfunc执行完毕。返回函数A不简单吗?代码是用python编写的,但这种对协程的讨论适用于任何语言,因为协程不是一种语言特性。而我们恰好以python为例,因为它足够简单。那么什么是协程?从普通函数到协程接下来,我们将从普通函数过渡到协程。与只有一个返回点的普通函数不同,协程可以有多个返回点。这是什么意思?voidfunc(){print("a")暂停并返回print("b")暂停并返回print("c")}在普通函数下,只有当执行print("c")语句后函数才会返回,但在协程中,print("a")执行后,func会因为“暂停并返回”代码而返回到调用函数。有些学生可能会感到困惑。这其中有什么神奇之处吗?我可以这样写return和return,像这样:这样return之后的代码就不会执行了。协程之所以神奇,是因为当我们从协程返回时,可以继续调用协程,在协程上一个返回点之后继续执行。这已经够神奇了,就像孙悟空说“叮”一样,函数暂停了:voidfunc(){print("a")setprint("b")setprint("c")}然后我们可以返回调用函数,当调用函数记住协程后,可以再次调用协程,协程会从之前的返回点继续执行。厉害,有没有,重点,别翻车。只是孙大圣用的公式“定”在编程语言中一般称为yield(其他语言可能有不同的实现,但本质是一样的)。需要注意的是,当一个普通的函数返回时,进程的地址空间中不会保存函数的运行时信息,但是协程返回后,需要保存函数的运行时信息,那么函数的运行时是做什么的state看起来像在内存中?你可以在这里参考这个问题。下面我们用实际代码来看一下协程。ShowMeTheCode下面我们用一个真实的例子来说明,语言使用python,不熟悉的同学不用担心,这里不会有理解的门槛。在python语言中,“fix”这个词还使用了关键字yield,所以我们的func函数就变成了:voidfunc(){print("a")yieldprint("b")yieldprint("c")}注意,此时我们的func不再是一个简单的函数,而是升级为协程,那么我们应该怎么使用呢,很简单:defA():co=func()#下一步获取协程(co)#调用协程print("infunctionA")#dosomethingnext(co)#再次调用协程我们可以看到虽然func函数没有return语句,也就是说虽然没有返回任何值,但是我们仍然可以写co=func()这样的代码表示co就是我们得到的协程。接下来,我们调用协程,使用next(co),运行函数A,看看执行到第3行的结果是什么:a很明显,正如我们所料,协程func是由于print("a")执行yieldpauses并返回到函数A。接下来是第4行。毫无疑问,函数A正在做它自己的事情,所以它会打印:ainfunctionA接下来是关键行。执行到第5行,再次调用协程应该打印什么?如果func是一个普通的函数,那么会执行func的第一行代码,即打印a。但是func不是普通的函数,而是协程。我们之前说过,协程会在之前的返回点继续运行,所以这里应该执行的是func函数第一次yield之后的代码,即print("b")。ainfunctionAb看,协程是一个很神奇的函数,它会记住之前的执行状态,再次调用时,会从上次的返回点继续执行。图解说明为了让大家更透彻的理解协程,我们再用图解的方式来看一下,首先是普通的函数调用:在这个图中,函数的指令序列用方框表示,如果functiondoesnotcall任何其他函数应该从上到下依次执行,但是函数中可以调用其他函数,所以它的执行不是简单的从上到下,箭头线表示执行流程的方向。从图中我们可以看出,我们首先来到了funcA这个函数,执行了一段时间后,发现又调用了另一个函数funcB。这时,控制权就转移到了这个函数上。执行完成后,我们回到main函数的调用点继续执行。这是一个普通的函数调用。接下来是协程。这里,我们还是先在funcA函数中执行,运行一段时间后调用协程,协程开始执行,直到第一个暂停点,然后像普通函数一样返回funcA函数,funcA函数执行又是一些代码和调用注意此时协程不同于普通函数。协程不是从第一条指令开始执行,而是从最后一个挂起点开始执行。执行一段时间后,遇到第二次暂停。此时协程像普通函数一样再次返回到funcA函数,funcA函数执行一段时间后整个程序结束。函数只是协程的特例怎么样?魔术不是魔术。与普通函数不同,协程可以知道它最后一次执行的位置。现在你应该明白了,协程会在函数挂起的时候保存函数的运行状态,并且可以从保存的状态恢复继续运行。听起来很熟悉吗?这不就是操作系统对线程的调度吗?线程也可以挂起。操作系统保存线程的运行状态,然后调度其他线程。之后,当线程再次被分配CPU时,它可以继续运行。就好像它从未被暂停过一样。只不过线程调度是由操作系统实现的,对程序员是不可见的,而协程是在用户态实现的,对程序员是可见的。这就是为什么有人说协程可以理解为用户态线程。这里应该有掌声。也就是说,程序员现在可以扮演操作系统的角色,你可以控制协程什么时候运行,什么时候挂起,也就是说协程的调度权掌握在自己手中。在协程这件事上,调度由你决定。当你在协程中写yield时,你想暂停协程,当你使用next()时,你又想再次运行协程。现在你应该明白为什么函数只是协程的一个特例了。函数实际上只是没有挂点的协程。协程的历史有些同学可能认为协程是一个比较新的技术。不过协程的概念早在1958年就已经提出来了,要知道线程的概念还没有提出来。1972年,一种编程语言终于实现了这个概念。这两种编程语言分别是Simula67和Scheme。但是协程的概念一直没有流行起来。甚至在1993年,就有人像考古一样写论文,挖掘协程这个古老的技术。因为这个时期没有线程,如果要在操作系统中编写并发程序,就不得不使用协程之类的技术。后来,线程开始出现,操作系统终于开始原生支持程序的并发执行。就是这样。协程逐渐淡出了程序员的视线。直到最近几年,随着互联网的发展,尤其是移动互联网时代的到来,服务器对高并发的要求越来越高,协程才再次回归到技术的主流。所有主流编程语言都已经支持或计划启动Coroutines。那么协程是如何实现的呢?协程是如何实现的让我们从问题的本质来思考这个问题。协程的本质是什么?其实就是一个可以挂起和恢复的功能。那么什么叫可以暂停,可以恢复呢?看过篮球比赛的同学肯定都知道(没看过的也一样)。篮球比赛也可以随时暂停。暂停的时候大家要记住球在哪一边,各自的位置是什么,比赛继续的时候大家回到各自的位置。裁判一吹哨,比赛继续进行,就好像比赛没有暂停一样。你看到问题的症结了吗?之所以可以暂停或继续比赛,是因为记录了比赛的状态(位置,球在哪一边),这里的状态就是计算机科学中常说的语境。回到协程。协程之所以可以挂起或者继续,那么它必须记录挂起时的状态,也就是上下文,继续运行的时候恢复它的上下文(状态),那么接下来很自然的问题就是,函数运行时的状态是什么?这个关键问题的答案在这篇文章《函数运行起来后在内存中是什么样子的》中,函数运行时的所有状态信息都位于函数运行时栈中。函数运行时栈就是我们需要保存的状态,也就是所谓的context,如图:从图中我们可以看出进程中只有一个线程,而进程中有四个栈帧堆栈区域。main函数调用A函数,函数A调用函数B,函数B调用函数C,当函数C运行时,整个流程的状态如图所示。既然我们知道函数的运行时状态存储在栈区的栈帧中,那么下一点就来了。由于函数的运行时状态是保存在栈区的栈帧中的,如果我们要暂停协程的运行,就必须保存整个栈帧的数据,那么整个栈帧中的数据应该保存在哪里呢?堆栈框架?想一想想一想这个问题,整个进程的哪一部分内存区域是专门用来长期存储数据的(进程生命周期)?大脑又一片空白了吗?不要留空!很明显,这里就是堆区,堆,我们可以在堆区保存栈帧,那么我们如何在堆区保存数据呢?我希望你还没有头晕。在堆区开辟空间就是我们常用的C语言中的malloc或者C++中的new。我们要做的就是在堆区申请一段空间,然后保存协程的整个栈区。当需要恢复协程的运行时,从堆区复制它来恢复函数的运行时状态。再想一想,为什么我们要来回复制数据这么麻烦?其实我们要做的就是直接在堆区开辟协程运行所需的栈帧空间,这样就不需要来回复制数据了,如图所示。从图中我们可以看出这个程序中开启了两个协程,并且这两个协程的栈区都分配在了堆上,这样我们就可以随时中断或者恢复协程的执行。有的同学可能会问,现在进程地址空间最顶层的栈区有什么作用呢?这个区域仍然是用来保存函数栈帧的,但是这些函数并不是运行在协程中而是普通线程中。现在你应该看到上图中实际上有3个执行流程:一个普通线程和两个协程。虽然有3个执行流程,但是我们创建了多少个线程呢?一个线程。现在你应该明白为什么需要使用协程了。使用协程,理论上我们可以开启无数个并发执行流。只要堆区有足够的空间,并且没有创建线程的开销,协程的所有调度和切换都会发生在用户态,这也是协程也被称为用户态线程的原因。掌声在哪里?所以即使你创建了N多个协程,从操作系统的角度来看仍然只有一个线程,也就是说协程对于操作系统来说是不可见的。这可能就是协程的概念比线程更早被提出的原因。可能写普通应用程序的程序员比写操作系统的程序员首先遇到多条并行流的需求。那个时候可能还没有操作系统的概念,或者说操作系统没有并行的需求,所以非操作系统的程序员只能自己实现执行流程,也就是协程。现在你应该对协程有了清晰的认识。综上所述,您应该已经了解协程是什么了。但是,还有一个问题没有解决。为什么协程的技术又回来了?协程适用于哪些场景?如何使用它们什么?本文转载自微信公众号《码农的荒岛求生》,可通过以下二维码关注。转载本文请联系码农荒岛求生公众号。