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

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

3天内不再提示

6种I/O模式告诉你协程的作用

Q4MP_gh_c472c21 来源:码农的荒岛求生 作者:陆小风 2022-05-24 15:23 次阅读

大家好,我是小风哥,今天我们来聊聊协程的作用。

假设磁盘上有10个文件,你需要读取的内存,那么你该怎么用代码实现呢?

在接着往下看之前,先自己想一想这个问题,看看自己能想出几种方法,各自有什么样的优缺点。想清楚了吗(还在看吗),想清楚了我们继续往下看。

最简单的方法——串行这可能是大多数同学都能想到的最简单方法,那就是一个一个的读取,读完一个接着读下一个。用代码表示是这样的:
for file in files:  result = file.read()  process(result)
是不是非常简单,我们假设每个文件读取需要1分钟,那么10个文件总共需要10分钟才能读取完成。这种方法有什么问题呢?实际上,这种方法只有一个问题,那就是除此之外,其它都是优点,比如:
  1. 代码简单,容易理解

  2. 可维护性好,这代码交给谁都能维护的了(论程序员的核心竞争力在哪里)

那么,慢的问题又该怎么解决呢?有的同学可能已经想到了,为啥要一个一个读取呢?并行读取不就可以加快速度了吗。
稍好的方法,并行那么,该怎么并行读取文件呢?显然,地球人都知道,线程就是用来并行的。我们可以同时开启10个线程,每个线程中读取一个文件。用代码实现就是这样的:

		def read_and_process(file): result = file.read() process(result) def main(): files = [fileA,fileB,fileC......] for file in files: create_thread(read_and_process, file).run() # 等待这些线程执行完成怎么样,是不是也非常简单。那么这种方法有什么问题吗?在开启10个线程这种问题规模下没有问题。现在我们把问题难度加大,假设有10000个文件,需要处理该怎么办呢?有的同学可能想10个文件和10000个文件有什么区别吗,直接创建10000个线程去读不可以吗?实际上,这里的问题其实是说创建多个线程有没有什么问题。我们知道,虽然线程号称“轻量级进程”,虽然是轻量级但当数量足够可观时依然会有性能问题。这里的问题主要有这样几个方面:
  1. 创建线程需要消耗系统资源,像内存等(想一想为什么?)

  2. 调度开销,尤其是当线程数量较多且都比较繁忙时(同样想一想为什么?)

  3. 创建多个线程不一定能加快I/O(如果此时设备处理能力已经饱和)

既然线程有这样那样的问题,那么还有没有更好的方法?答案是肯定的,并行编程不一定只能依赖线程这种技术,关于并发编程可以用哪些技术实现的详细讨论请参考《高性能服务器是如何实现的》。这里的答案就是基于事件驱动编程技术。
事件驱动 + 异步没错,即使在单个线程中,使用事件驱动+异步也可以实现IO并行处理,Node.js就是非常典型的例子。为什么单线程也可以做到并行呢?这是基于这样两个事实:
  1. 相对于CPU的处理速度来说,IO是非常慢的

  2. IO不怎么需要计算资源

因此,当我们发起IO操作后为什么要一直等着IO执行完成呢?在IO执行完之前的这段时间处理其它IO难道不香吗这就是为什么单线程也可以并行处理多个IO的本质所在。回到我们的例子,该怎样用事件驱动+异步来改造上述程序呢?实际上非常简单。首先,我们需要创建一个event loop,这个非常简单:

		event_loop = EventLoop()然后,我们需要往event loop中加入原材料,也就是需要监控的event,就像这样:

		def add_to_event_loop(event_loop, file): file.asyn_read() # 文件异步读取 event_loop.add(file)注意,当执行file.asyn_read这行代码时会立即返回,不会阻塞线程,当这行代码返回时可能文件还没有真正开始读取,这就是所谓的异步。file.asyn_read这行代码的真正目的仅仅是发起IO,而不是等待IO执行完成。此后,我们将该IO放到event loop中进行监控,也就是event_loop.add(file)这行代码的作用。一切准备就绪,接下来就可以等待event的到来了:

		while event_loop: file = event_loop.wait_one_IO_ready() process(file.result)我们可以看到,event_loop会一直等待直到有文件读取完成(event_loop.wait_one_IO_ready())。这时,我们就能得到读完的文件了,接下来处理即可。全部代码如下所示:
 def add_to_event_loop(event_loop, file):   file.asyn_read() # 文件异步读取   event_loop.add(file)
def main():files=[fileA,fileB,fileC...]  event_loop = EventLoop()  for file in files:      add_to_event_loop(event_loop, file)        while event_loop:     file = event_loop.wait_one_IO_ready()     process(file.result)

多线程 VS 单线程 + event loop接下来,我们看下程序执行的效果。在多线程情况下,假设有10个文件,每个文件读取需要1秒,那么很简单,并行读取10个文件需要1秒。那么,对于单线程+event loop呢?我们再次看下event loop + 异步版本的代码:

		def add_to_event_loop(event_loop, file): file.asyn_read() # 文件异步读取 event_loop.add(file) def main(): files = [fileA,fileB,fileC......] event_loop = EventLoop() for file in files: add_to_event_loop(event_loop, file)  while event_loop: file = event_loop.wait_one_IO_ready() process(file.result)对于add_to_event_loop,由于文件异步读取,因此该函数可以瞬间执行完成,真正耗时的函数其实就是event loop的等待函数,也就是这样:

		file = event_loop.wait_one_IO_ready()我们知道,一个文件的读取耗时是1秒,因此该函数在1s后才能返回,但是,但是,接下来是重点。但是,虽然该函数wait_one_IO_ready会等待1s,不要忘了,我们利用这两行代码同时发起了10个IO操作请求。
for file in files:  add_to_event_loop(event_loop, file)
因此,在event_loop.wait_one_IO_ready等待的1s期间,剩下的9个IO也完成了。也就是说,event_loop.wait_one_IO_ready函数只是在第一次循环时会等待1s,但此后的9次循环会直接返回,原因就在于剩下的9个IO也完成了因此,整个程序的执行耗时也是1秒。是不是很神奇,我们只用一个线程就达到了10个线程的效果。这就是event loop + 异步的威力所在。
一个好听的名字:Reactors模式本质上,我们上述给出的event loop简单代码片段做的事情本质上和生物一样:给出刺激,做出反应。我们这里的给出event,然后处理event。这本质上就是所谓的Reactors模式。现在你应该明白所谓的Reactors模式是怎么一回事了吧。所谓的一些看上去复杂的异步框架,其核心不过就是这里给出的代码片段,只是这些框架可以支持更加复杂的多阶段任务处理,以及各种类型的IO。而我们这里给出的代码片段,只能处理文件读取这一类IO。
把回调也加进来如果我们需要处理各种类型的IO上述代码片段会有什么问题吗?问题就在于上述代码片段就不会这么简单了,针对不同类型会有不同的处理方法。因此,上述process方法需要判断IO类型然后有针对性的处理,这会使得代码越来越复杂,越来越难以维护。幸好我们也有应对策略,这就是回调。关于回调函数,请参考这篇《程序员应如何理解回调函数》。我们可以把IO完成后的处理任务封装到回调函数中,然后和IO一并注册到event loop就像这样:

		def IO_type_1(event_loop, io): io.start()  def callback(result): process_IO_type_1(result)  event_loop.add((io, callback))这样,event_loop在检测到有IO完成后就可以把该IO和关联的callback处理函数一并检索出来,直接调用callback函数就可以了。

		while event_loop: io, callback = event_loop.wait_one_IO_ready() callback(io.result)看到了吧,这样event_loop内部就极其简洁了,even_loop根本就不关心该怎么处理该IO结果,这是注册的callback该关心的事情,event_loop需要做的仅仅就是拿到event以及相应的处理函数callback,然后调用该callback函数就可以了。现在我们可以同单线程来并发编程了,也使用callback对IO处理进行了抽象,使得代码更加容易维护,想想看还有没有什么问题?
		
回调函数的问题虽然回调函数使得event loop内部更加简洁,但依然有其它问题,让我们来仔细看看回调函数:

		def start_IO_type_1(event_loop, io): io.start()  def callback(result): process_IO_type_1(result)  event_loop.add((io, callback))从上述代码中你能看到什么问题吗?在上述代码中,一次IO处理过程被分为了两个部分:
  1. 发起IO

  2. IO处理

其中,第2部分放到了回调函数中,这样的异步处理天然不容易理解,这和我们熟悉的发起IO,等待IO完成、处理IO结果的同步模块有很大差别。这里的给的例子很简单,所以你可能不以为意,但是当处理的任务非常复杂时,可能会出现回调函数中嵌套回调函数,也就是回调地狱,这样的代码维护起来会让你怀疑为什么要称为一名苦逼的码农。
问题出在哪里让我们再来仔细的看看问题出在了哪里?同步编程模式下很简单,但是同步模式下发起IO,线程会被阻塞,这样我们就不得不创建多个线程,但是创建过多线程又会有性能问题。这样为了发起IO后不阻塞当前线程我们就不得不采用异步编程+event loop。在这种模式下,异步发起IO不会阻塞调用线程,我们可以使用单线程加异步编程的方法来实现多线程效果,但是在这种模式下处理一个IO的流程又不得不被拆分成两部分,这样的代码违反程序员直觉,因此难以维护。那么很自然的,有没有一种方法既能有同步编程的简单理解又会有异步编程的非阻塞呢?答案是肯定的,这就是协程。关于协程请参考《程序员应如何理解协程》。
Finally!终于到了协程利用协程,我可以以同步的形式来异步编程。这是什么意思呢?我们之所以采用异步编程是为了发起IO后不阻塞当前线程,而是用协程,程序员可以自行决定在什么时刻挂起当前协程,这样也不会阻塞当前线程。而协程最棒的一点就在于挂起后可以暂存执行状态恢复运行后可以在挂起点继续运行,这样我们就不再需要像回调那样将一个IO的处理流程拆分成两部分了。因此,我们可以在发起异步IO,这样不会阻塞当前线程,同时在发起异步IO后挂起当前协程,当IO完成后恢复该协程的运行。这样一来,我们就可以实现同步的方式来异步编程了。接下来,我们就用协程来改造一下回调版本的IO处理方式:

		def start_IO_type_1(io): io.start() # IO异步请求 yield # 暂停当前协程  process_IO_type_1(result) # 处理返回结果此后,我们要把该协程放到event loop中监控起来:

		def add_to_event_loop(io, event_loop): coroutine = start_IO_type_1(io) next(coroutine) event_loop.add(coroutine)最后,当IO完成后event loop检索出相应的协程并恢复其运行:

		while event_loop: coroutine = event_loop.wait_one_IO_ready() next(coroutine)现在你应该看出来了吧,上述代码中没有回调,也没有把处理IO的流程拆成两部分,整体的代码都是以同步的方式来编写,最棒的是依然能达到异步的效果。实际上你会看到,采用协程后我们依然需要基于事件编程的event loop,因为本质上协程并没有改变IO的异步处理本质,只要IO是异步处理的那么我们就必须依赖event loop来监控IO何时完成,只不过我们采用协程消除了对回调的依赖,整体编程方式上还是采用程序员最熟悉也最容易理解的同步方式。
		
总结看上去简简单单的IO,实际上一点都不简单。为了高效进行IO操作,我们采用的技术是这样演进的:
  1. 单线程串行 + 阻塞式IO(同步)

  2. 多线程并行 + 阻塞式IO(并行)

  3. 单线程 + 非阻塞式IO(异步) + event loop

  4. 单线程 + 非阻塞式IO(异步) + event loop + 回调

  5. Reactor模式(更好的单线程 + 非阻塞式IO+ event loop + 回调)

  6. 单线程 + 非阻塞式IO(异步) + event loop + 协程

最终,我们采用协程技术获取到了异步编程的高效以及同步编程的简单理解,这也是当今高性能服务器常用的一种技术组合。希望这篇文章能对你理解高效IO有所帮助。

审核编辑 :李倩


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

    关注

    30

    文章

    4553

    浏览量

    66665
  • 模式
    +关注

    关注

    0

    文章

    63

    浏览量

    13268

原文标题:6种I/O模式告诉你,协程到底有什么用?

文章出处:【微信号:gh_c472c2199c88,微信公众号:嵌入式微处理器】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    STM32F407ZET6操作I/O端口和串口均无反应,为什么?

    本来是想实现串口通过485发送和接收数据,后来发现单片机USART2发送数据,端口无响应,然后换了一个I/O口,也发现没反应,不晓得是不是哪里配置的不对?程序调试,打断点走不到断点的地方,单步可以运行。请高手们帮看看是啥问题,在线等待。
    发表于 04-08 07:24

    鸿蒙原生应用开发-ArkTS语言基础类库多线程I/O密集型任务开发

    使用异步并发可以解决单次I/O任务阻塞的问题,但是如果遇到I/O密集型任务,同样会阻塞线程中其它任务的执行,这时需要使用多线程并发能力来进行解决。
    发表于 03-21 14:57

    在CapSense按钮触发时是否有可能重新配置I/O的操作?

    能否告诉我在 CapSense 按钮触发时是否有可能重新配置 I/O 的操作? 我想使用一个 CapSense 按钮并启用/禁用电路上的另一个 IC。 这意味着,如果按下 CapSense 按钮,输出将永久保持高电平,而如果再次
    发表于 02-23 06:20

    CY7C65215如何在快速模式和慢速模式之间切换吗?

    我有一个关于 CY7C65215 的问题。 告诉我如何在快速模式和慢速模式之间切换吗? 从英飞凌的惠普那里获取软件(配置实用工具), 我对有必要重写 CY7C65215 的内部闪存
    发表于 02-22 07:04

    求助,关于CX3上未使用的I/O引脚的简单问题

    这是一个关于 CX3 上未使用的 I/O 引脚的简单问题。CYUSB306X 数据表(第 20 页)建议 \" 应使用内部上拉电阻 \" 将所有未使用的 I/O 拉高。 但是,我
    发表于 02-22 06:55

    应用方案:MCU通用I/O引脚扩展

    MCU通用I/O引脚扩展 低端MCU由于I/O口数量不足导致部分功能无法实现,用户需要使用数字集成芯片进行扩展,如74LS系列移位寄存器,但是这种集成芯片也会由于引脚数量限制而无法确保
    发表于 01-08 09:35

    为什么无法改变单片机I/O输出电平?

    请问一下我在使用51最小系统板做流水灯的时候通过程序无法改变I/O输出电平是怎么回事,I/O持续输出5V高电平,但是把芯片换到另一个基座就可以改变,请问哪里可能出问题了
    发表于 09-27 07:38

    HarmonyOS CPU与I/O密集型任务开发指导

    workerPort.close(); 二、I/O密集型任务开发指导 使用异步并发可以解决单次I/O任务阻塞的问题,但是如果遇到I/
    发表于 09-26 16:29

    CW32F030x6/x8数据手册

    PWM 定时器。CW32F030x6/x8 可以在 -40° C 到 105° C 的温度范围内工作,供电电压宽达 1.65V ~ 5.5V。支持 Sleep 和DeepSleep 两低功耗工作模式
    发表于 09-14 07:19

    I/ O检测时如何使用SysTick进行计数

    应用程序 : 当 I/ O 检测时, 请使用 SysTick 进行计数。 如果在一段时间之间发生反弹, 请不要响应以避免噪音 。 BSP 版本: NUC123系列 BSP CMSIS
    发表于 08-30 08:03

    请问如何透过PinView确认I/O是否有漏电流?

    如何透过PinView确认I/O是否有漏电流?
    发表于 06-26 07:08

    M451 ADC有3工作模式,是如何设置的?

    M451,ADC有3工作模式,是如何设置的吗? ADC有3工作模式:单次、单次循环和连续循环模式。  单次:就是在某个使能的通道上完成
    发表于 06-25 11:30

    NUC123 SPI如何使用Dual I/O功能?

    控制的單片機要如何讀寫SPI Flash呢? 這個範例代碼分別提供使用Dual I/O功能的Master和Slave的代碼, 連接方式如下圖. 首先在Dual I/O
    发表于 06-21 07:13

    请问如何透过PinView确认I/O是否有漏电流?

    如何透过 PinView 确认 I/O 是否有漏电流? 功能介绍: PinView 能够用来确认 GPIO 的状态,当侦测到不正常的状态时,会将CPIO 的号码使用红色标注提醒使用者,下列是五个
    发表于 06-20 08:24

    PCA***未使用的I/O端口的原因?

    您好,PCA***上未使用的I/O端口应该如何端接?这些可以悬空/不连接还是应该上拉/下拉?谢谢!
    发表于 05-09 06:07