写点什么

【计算机内功修炼】九:程序员应如何理解协程

发布于: 2021 年 02 月 23 日
【计算机内功修炼】九:程序员应如何理解协程

作为程序员,想必你多多少少听过协程这个词,这项技术近年来越来越多的出现在程序员的视野当中,尤其高性能高并发领域。当你的同学、同事提到协程时如果你的大脑一片空白,对其毫无概念。。。



那么这篇文章正是为你量身打造的。


话不多说,今天的主题就是作为程序员,你应该如何彻底理解协程。


普通的函数


我们先来看一个普通的函数,这个函数非常简单:


def func():   print("a")   print("b")   print("c")
复制代码


这是一个简单的普通函数,当我们调用这个函数时会发生什么?


  1. 调用 func

  2. func 开始执行,直到 return

  3. func 执行完成,返回函数 A


是不是很简单,函数 func 执行直到返回,并打印出:


abc
复制代码


So easy,有没有,有没有!



很好!


注意这段代码是用 python 写的,但本篇关于协程的讨论适用于任何一门语言,我们只不过恰好使用了 python 来用作示例,因为其足够简单。


那么协程是什么呢?


从普通函数到协程


接下来,我们就要从普通函数过渡到协程了。


和普通函数只有一个返回点不同,协程可以有多个返回点


这是什么意思呢?

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(其它语言中可能会有不同的实现,但本质都是一样的)。


需要注意的是,当普通函数返回后,进程的地址空间中不会再保存该函数运行时的任何信息,而协程返回后,函数的运行时信息是需要保存下来的,那么函数的运行时状态到底在内存中是什么样子呢,关于这个问题你可以参考这里


接下来,我们就用实际的代码看一看协程。


show me the code


下面我们使用一个真实的例子来讲解,语言采用 python,不熟悉的同学不用担心,这里不会有理解上的门槛。


在 python 语言中,这个“定”字同样使用关键词 yield,这样我们的 func 函数就变成了:


void func() {  print("a")  yield  print("b")  yield  print("c")}
复制代码


注意,这时我们的 func 就不再是简简单单的函数了,而是升级成为了协程,那么我们该怎么使用呢,很简单:


1 def A():2   co = func() # 得到该协程3   next(co)    # 调用协程4   print("in function A") # do something5   next(co)    # 再次调用该协程
复制代码


我们看到虽然 func 函数没有 return 语句,也就是说虽然没有返回任何值,但是我们依然可以写 co = func()这样的代码,意思是说 co 就是我们拿到的协程了。


接下来我们调用该协程,使用 next(co),运行函数 A 看看执行到第 3 行的结果是什么:


a
复制代码


显然,和我们的预期一样,协程 func 在 print("a")后因执行 yield 而暂停并返回函数 A。


接下来是第 4 行,这个毫无疑问,A 函数在做一些自己的事情,因此会打印:


ain functino A
复制代码


接下来是重点的一行,当执行第 5 行再次调用协程时该打印什么呢?


如果 func 是普通函数,那么会执行 func 的第一行代码,也就是打印 a。


但 func 不是普通函数,而是协程,我们之前说过,协程会在上一个返回点继续运行,因此这里应该执行的是 func 函数第一个 yield 之后的代码,也就是 print("b")。


ain functino Ab
复制代码


看到了吧,协程是一个很神奇的函数,它会自己记住之前的执行状态,当再次调用时会从上一次的返回点继续执行。


神奇不神奇,厉害不厉害!



Very Good.


图形化解释


为了让你更加彻底的理解协程,我们使用图形化的方式再看一遍,首先是普通的函数调用:



在该图中,方框内表示该函数的指令序列,如果该函数不调用任何其它函数,那么应该从上到下依次执行,但函数中可以调用其它函数,因此其执行并不是简单的从上到下,箭头线表示执行流的方向。


从图中我们可以看到,我们首先来到 funcA 函数,执行一段时间后发现调用了另一个函数 funcB,这时控制转移到该函数,执行完成后回到 main 函数的调用点继续执行。


这是普通的函数调用。


接下来是协程。



在这里,我们依然首先在 funcA 函数中执行,运行一段时间后调用协程,协程开始执行,直到第一个挂起点,此后就像普通函数一样返回 funcA 函数,funcA 函数执行一些代码后再次调用该协程,注意,协程这时就和普通函数不一样了,协程并不是从第一条指令开始执行而是从上一次的挂起点开始执行,执行一段时间后遇到第二个挂起点,这时协程再次像普通函数一样返回 funcA 函数,funcA 函数执行一段时间后整个程序结束。



函数只是协程的一种特例


怎么样,神奇不神奇,和普通函数不同的是,协程能知道自己上一次执行到了哪里


现在你应该明白了吧,协程会在函数被暂停运行时保存函数的运行状态,并可以从保存的状态中恢复并继续运行。


很熟悉的味道有没有,这不就是操作系统对线程的调度嘛,线程也可以被暂停,操作系统保存线程运行状态然后去调度其它线程,此后该线程再次被分配 CPU 时还可以继续运行,就像没有被暂停过一样。


只不过线程的调度是操作系统实现的,这些对程序员都不可见,而协程是在用户态实现的,对程序员可见。


这就是为什么有的人说可以把协程理解为用户态线程的原因。


此处应该有掌声。


也就是说现在程序员可以扮演操作系统的角色了,你可以自己控制协程在什么时候运行,什么时候暂停,也就是说协程的调度权在你自己手上。


在协程这件事儿上,调度你说了算


当你在协程中写下 yield 的时候就是想要暂停改协程,当使用 next()时就是要再次运行该协程。


现在你应该理解为什么说函数只是协程的一种特例了吧,函数其实只是没有挂起点的协程而已。


协程的历史


有的同学可能认为协程是一种比较新的技术,然而其实协程这种概念早在 1958 就已经提出来了,要知道这时线程的概念都还没有提出来


到了 1972 年,终于有编程语言实现了这个概念,这两门编程语言就是 Simula 67 以及 Scheme。



但协程这个概念始终没有流行起来,甚至在 1993 年还有人考古一样专门写论文挖出协程这种古老的技术。


因为这一时期还没有线程,如果你想在操作系统写出并发程序那么你将不得不使用类似协程这样的技术,后来线程开始出现,操作系统终于开始原生支持程序的并发执行,就这样,协程逐渐淡出了程序员的视线。


直到近些年,随着互联网的发展,尤其是移动互联网时代的到来,服务端对高并发的要求越来越高,协程再一次重回技术主流,各大编程语言都已经支持或计划开始支持协程。


现在你应该对协程有一个清晰的认知了吧。



总结


到这里你应该已经理解协程到底是怎么一回事,但是,依然有一个问题没有解决,为什么协程这种技术又一次重回视线,协程适用于什么场景下呢?该怎么使用呢?


关于这些问题,下一篇文章将会给你答案。


希望这篇对你理解协程有所帮助。


发布于: 2021 年 02 月 23 日阅读数: 48
用户头像

还未添加个人签名 2019.01.07 加入

公众号:码农的荒岛求生

评论

发布
暂无评论
【计算机内功修炼】九:程序员应如何理解协程