0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

什么是协程?如何彻底理解协程?

dyquk4xk2p3d 来源:码农的荒岛求生 2023-10-08 09:58 次阅读

普通的函数

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

def func():
   print("a")
   print("b")
   print("c")
这是一个简单的普通函数,当我们调用这个函数时会发生什么?

调用func

func开始执行,直到return

func执行完成,返回函数A

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

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

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

ShowMe The Code

下面我们使用一个真实的例子来讲解,语言采用python,不熟悉的同学不用担心,这里不会有理解上的门槛。 在python语言中,这个“定”字同样使用关键词yield,这样我们的func函数就变成了:

void func() {
  print("a")
  yield
  print("b")
  yield
  print("c")
}
注意,这时我们的func就不再是简简单单的函数了,而是升级成为了协程,那么我们该怎么使用呢,很简单:
defA():
co=func()#得到该协程
next(co)#调用协程
print("infunctionA")#dosomething
next(co)#再次调用该协程
我们看到虽然func函数没有return语句,也就是说虽然没有返回任何值,但是我们依然可以写co = func()这样的代码,意思是说co就是我们拿到的协程了。 接下来我们调用该协程,使用next(co),运行函数A看看执行到第3行的结果是什么:
a
显然,和我们的预期一样,协程func在print("a")后因执行yield而暂停并返回函数A。 接下来是第4行,这个毫无疑问,A函数在做一些自己的事情,因此会打印:
a
infunction A
接下来是重点的一行,当执行第5行再次调用协程时该打印什么呢? 如果func是普通函数,那么会执行func的第一行代码,也就是打印a。

但func不是普通函数,而是协程,我们之前说过,协程会在上一个返回点继续运行,因此这里应该执行的是func函数第一个yield之后的代码,也就是print("b")。
a
infunctionA
b
看到了吧,协程是一个很神奇的函数,它会自己记住之前的执行状态,当再次调用时会从上一次的返回点继续执行。

图形化解释

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

3184bf88-5e6d-11ee-939d-92fbcf53809c.png

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

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

这是普通的函数调用。 接下来是协程。

318ceabe-5e6d-11ee-939d-92fbcf53809c.png

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

319b5216-5e6d-11ee-939d-92fbcf53809c.png

函数只是协程的一种特例

怎么样,神奇不神奇,和普通函数不同的是,协程能知道自己上一次执行到了哪里。 现在你应该明白了吧,协程会在函数被暂停运行时保存函数的运行状态,并可以从保存的状态中恢复并继续运行。

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

只不过线程的调度是操作系统实现的,这些对程序员都不可见,而协程是在用户态实现的,对程序员可见。 这就是为什么有的人说可以把协程理解为用户态线程的原因。 此处应该有掌声。

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

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

当你在协程中写下yield的时候就是想要暂停该协程,当使用next()时就是要再次运行该协程。 现在你应该理解为什么说函数只是协程的一种特例了吧,函数其实只是没有挂起点的协程而已。

协程的历史

有的同学可能认为协程是一种比较新的技术,然而其实协程这种概念早在1958年就已经提出来了,要知道这时线程的概念都还没有提出来。 到了1972年,终于有编程语言实现了这个概念,这两门编程语言就是Simula 67 以及Scheme。

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

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

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

那么协程到底是如何实现的呢?

协程是如何实现的

让我们从问题的本质出发来思考这个问题。 协程的本质是什么呢? 其实就是可以被暂停以及可以被恢复运行的函数。

那么可以被暂停以及可以被恢复意味着什么呢? 看过篮球比赛的同学想必都知道(没看过的也能知道),篮球比赛也是可以被随时暂停的,暂停时大家需要记住球在哪一方,各自的站位是什么,等到比赛继续的时候大家回到各自的位置,裁判哨子一响比赛继续,就像比赛没有被暂停过一样。

看到问题的关键了吗,比赛之所以可以被暂停也可以继续是因为比赛状态被记录下来了(站位、球在哪一方),这里的状态就是计算机科学中常说的上下文,context。

回到协程。

协程之所以可以被暂停也可以继续,那么一定要记录下被暂停时的状态,也就是上下文,当继续运行的时候要恢复其上下文(状态),那么接下来很自然的一个问题就是,函数运行时的状态是什么? 这个关键的问题的答案就在《函数运行起来后在内存中是什么样子的》这篇文章中,函数运行时所有的状态信息都位于函数运行时栈中。

函数运行时栈就是我们需要保存的状态,也就是所谓的上下文,如图所示:

31d5b2bc-5e6d-11ee-939d-92fbcf53809c.png

从图中我们可以看出,该进程中只有一个线程,栈区中有四个栈帧,main函数调用A函数,A函数调用B函数,B函数调用C函数,当C函数在运行时整个进程的状态就如图所示。

现在我们已经知道了函数的运行时状态就保存在栈区的栈帧中,接下来重点来了哦。 既然函数的运行时状态保存在栈区的栈帧中,那么如果我们想暂停协程的运行就必须保存整个栈帧的数据,那么我们该将整个栈帧中的数据保存在哪里呢? 想一想这个问题,整个进程的内存区中哪一块是专门用来长时间(进程生命周期)存储数据的?是不是大脑又一片空白了?

先别空白! 很显然,这就是堆区啊,heap,我们可以将栈帧保存在堆区中,那么我们该怎么在堆区中保存数据呢?希望你还没有晕,在堆区中开辟空间就是我们常用的C语言中的malloc或者C++中的new。 我们需要做的就是在堆区中申请一段空间,让后把协程的整个栈区保存下,当需要恢复协程的运行时再从堆区中copy出来恢复函数运行时状态。

再仔细想一想,为什么我们要这么麻烦的来回copy数据呢? 实际上,我们需要做的是直接把协程的运行需要的栈帧空间直接开辟在堆区中,这样都不用来回copy数据了,如图所示。

31f8d224-5e6d-11ee-939d-92fbcf53809c.png

从图中我们可以看到,该程序中开启了两个协程,这两个协程的栈区都是在堆上分配的,这样我们就可以随时中断或者恢复协程的执行了。 有的同学可能会问,那么进程地址空间最上层的栈区现在的作用是什么呢? 这一区域依然是用来保存函数栈帧的,只不过这些函数并不是运行在协程而是普通线程中的。

现在你应该看到了吧,在上图中实际上有3个执行流:

一个普通线程

两个协程

虽然有3个执行流但我们创建了几个线程呢? 一个线程

现在你应该明白为什么要使用协程了吧,使用协程理论上我们可以开启无数并发执行流,只要堆区空间足够,同时还没有创建线程的开销,所有协程的调度、切换都发生在用户态,这就是为什么协程也被称作用户态线程的原因所在。 掌声在哪里?

因此即使你创建了N多协程,但在操作系统看来依然只有一个线程,也就是说协程对操作系统来说是不可见的。 这也许是为什么协程这个概念比线程提出的要早的原因,可能是写普通应用的程序员比写操作系统的程序员最先遇到需要多个并行流的需求,那时可能都还没有操作系统的概念,或者操作系统没有并行这种需求,所以非操作系统程序员只能自己动手实现执行流,也就是协程。 现在你应该对协程有一个清晰的认知了吧。






审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • cpu
    cpu
    +关注

    关注

    68

    文章

    10863

    浏览量

    211768
  • C++语言
    +关注

    关注

    0

    文章

    147

    浏览量

    6992
  • python
    +关注

    关注

    56

    文章

    4797

    浏览量

    84689

原文标题:彻底理解什么是协程!

文章出处:【微信号:良许Linux,微信公众号:良许Linux】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    谈谈的那些事儿

    随着异步编程的发展以及各种并发框架的普及,作为一种异步编程规范在各类语言中地位逐步提高。我们不单单会在自己的程序中使用,各类框架如fastapi,aiohttp等也都是基于异步
    的头像 发表于 01-26 11:36 1118次阅读
    谈谈<b class='flag-5'>协</b><b class='flag-5'>程</b>的那些事儿

    和线程有什么区别

    和线程的区别和线程的共同目的之一是实现系统资源的上下文调用,不过它们的实现层级不同;线程(Thraed)是比进程小一级的的运行单位,多线程实现系统资源上下文调用,是编程语言交付
    发表于 12-10 06:23

    Python中的多核CPU共享数据之详解

    又称微线程,coroutne,是一种用户态的轻量级线程。通俗点讲就是周末我在家里休息,假如我先洗漱,再煮饭,再下载电影看会很慢,用了
    的头像 发表于 12-07 10:23 6631次阅读
    Python中的多核CPU共享数据之<b class='flag-5'>协</b><b class='flag-5'>程</b>详解

    关于C++ 20最全面详解

    花了一两周的时间后,我想写写 C++20 的基本用法,因为 C++ 的让我感到很奇怪,写一个
    的头像 发表于 04-12 11:10 1.3w次阅读
    关于C++ 20<b class='flag-5'>协</b><b class='flag-5'>程</b>最全面详解

    Python后端项目的是什么

    最近公司 Python 后端项目进行重构,整个后端逻辑基本都变更为采用“异步”的方式实现。看着满屏幕经过 async await(在 Python 中的实现)修饰的代码,我顿时
    的头像 发表于 09-23 14:38 1332次阅读

    Python与JavaScript的对比及经验技巧

    对这两个语言有兴趣的新人理解和吸收。 共同诉求随着 cpu 多核化,都需要实现由于自身历史原因(单线程环境)下的并发功能 简化代码,避免回调地狱,关键字支持 有效利用操作系统资源和硬件:相比线程,占用资源更少,上下文更快 什
    的头像 发表于 10-20 14:30 1937次阅读

    通过例子由浅入深的理解yield

    send:send() 方法致使程前进到下一个yield 语句,另外,生成器可以作为使用
    的头像 发表于 08-23 11:12 2031次阅读

    使用channel控制数量

    goroutine 是轻量级线程,调度由 Go 运行时进行管理的。Go 语言的并发控制主要使用关键字 go 开启 goroutine。Go (Goroutine)之间通过信道(
    的头像 发表于 09-19 15:06 1140次阅读

    详解Linux线程、线程与异步编程、与异步

    不是系统级线程,很多时候被称为“轻量级线程”、“微线程”、“纤(fiber)”等。简单来说可以认为
    的头像 发表于 03-16 15:49 985次阅读

    的概念及的挂起函数介绍

    是一种轻量级的线程,它可以在单个线程中实现并发执行。与线程不同,不需要操作系统的上下文切换,因此可以更高效地使用系统资源。Kotlin
    的头像 发表于 04-19 10:20 892次阅读

    Kotlin实战进阶之筑基篇3

    的概念在1958年就开始出现(比线程还早), 目前很多语言开始原生支, Java 没有原生但是大型公司都自己或者使用第三方库来支持
    的头像 发表于 05-30 16:26 700次阅读

    FreeRTOS任务与介绍

    FreeRTOS 中应用既可以使用任务,也可以使用(Co-Routine),或者两者混合使用。但是任务和协使用不同的API函数,因此不能通过队列(或信号量)将数据从任务发送给
    的头像 发表于 09-28 11:02 998次阅读

    的作用、结构及原理

    本文介绍了的作用、结构、原理,并使用C++和汇编实现了64位系统下的池。文章内容避免了
    的头像 发表于 11-08 16:39 1141次阅读
    <b class='flag-5'>协</b><b class='flag-5'>程</b>的作用、结构及原理

    的实现与原理

    前言 这个概念很久了,好多程序员是实现过这个组件的,网上关于的文章,博客,论坛都是汗牛充栋,在知乎,github上面也有很多大牛写了关于
    的头像 发表于 11-10 10:57 442次阅读

    Linux线程、线程与异步编程、与异步介绍

    不是系统级线程,很多时候被称为“轻量级线程”、“微线程”、“纤(fiber)”等。简单来说可以认为
    的头像 发表于 11-11 11:35 1167次阅读
    Linux线程、线程与异步编程、<b class='flag-5'>协</b><b class='flag-5'>程</b>与异步介绍