本篇文章主要是讲解asyncio模块的实现原理. 这个系列还有另外两篇文章:

一. asyncio模块简介

  asyncio是python3.4开始内置的一个标准库, 可以用于编写异步的并发代码, 因此非常适合用在IO密集型操作.

  现在运行如下代码:

  1. import asyncio
  2. import time
  3.  
  4. async def task(i):
  5. print('task{} start at {}'.format(i, time.ctime()))
  6. # asyncio.sleep的效果与time.sleep类似, 让程序睡眠n秒
  7. await asyncio.sleep(3)
  8. print('task{} end at {}'.format(i, time.ctime()))
  9.  
  10. tasks = asyncio.wait([task(i) for i in range(3)])
  11. asyncio.run(tasks)

运行结果如下:

三个任务实际是处于同一线程的, 但它们的执行顺序不是start->end->start->end这种串行模式, 而是几乎同时开始, 同时结束, asyncio模块的作用就是, 使用异步的方式实现单线程并发的效果. 最简单的使用步骤如下:

  • 首先, 在定义函数的时候使用关键字async, 这个函数就不是个普通函数了, 调用的时候不会执行内部代码, 而是返回一个coroutine对象, 即协程, 这一点与生成器函数类似.
  • 然后, 在协程函数中的耗时操作前面加上await关键字, 注意await后面必须是可等待对象, 比如asyncio.sleep(n), 可等待对象在本文的第二节有详细的讲解.
  • 最后, 调用asyncio.wait将协程列表打包, 打包结果给asyncio.run运行即可.

二. asyncio实现原理

  要理解asyncio的原理, 需要理解如下几个概念: 协程, 事件循环, future/task. 其中协程就是用户自己定义的任务, 事件循环负责监听事件和回调, future/task则主要负责管理回调, 以及驱动协程.

1. 事件循环

  事件循环负责同时对多个事件进行监听, 当监听到事件时, 就调用对应的回调函数, 进而驱动不同的任务. 上一节代码最后的asyncio.run, 其本质就是创建一个事件循环, 然后一直运行事件循环, 直到所有任务结束为止.

  首先看看上篇文章最后的爬虫代码:

  1. import select
  2. import socket
  3. import time
  4.  
  5. req = 'GET / HTTP/1.0\r\nHost:cn.bing.com\r\n\r\n'.encode('utf8')
  6. address = ('cn.bing.com', 80)
  7. db = []
  8.  
  9. class GenCrawler:
  10.  
  11. '''
  12. 这里使用一个类将生成器封装起来,如果要驱动生成器,就调用next_step方法
  13. 另外,这个类还可以获取到使用的socket对象
  14. '''
  15.  
  16. def __init__(self):
  17. self.sock = socket.socket()
  18. self.sock.setblocking(0)
  19. self._gen = self._crawler()
  20.  
  21. def next_step(self):
  22. next(self._gen)
  23.  
  24. def _crawler(self):
  25. self.sock.connect_ex(address)
  26. yield
  27. self.sock.send(req)
  28. response = b''
  29. while 1:
  30. yield
  31. chunk = self.sock.recv(1024)
  32. if chunk == b'':
  33. self.sock.close()
  34. break
  35. else:
  36. response += chunk
  37. db.append(response)
  38.  
  39. def event_loop(crawlers):
  40. # 首先,建立sock与crawler对象的映射关系,便于由socket对象找到对应的crawler对象
  41. # 建立映射的同时顺便调用crawler的next_step方法,让内部的生成器运行起来
  42. sock_to_crawler = {}
  43. for crawler in crawlers:
  44. sock_to_crawler[crawler.sock] = crawler
  45. crawler.next_step()
  46.  
  47. # select.select需要传入三个列表,分别对应要监听的可读,可写和错误事件的socket对象集合
  48. readable = []
  49. writeable = [crawler.sock for crawler in crawlers]
  50. errors = []
  51. while 1:
  52. rs, ws, es = select.select(readable, writeable, errors)
  53. for sock in ws:
  54. # 当socket对象连接到服务器时,会创建可读缓冲区和可写缓冲区
  55. # 由于可写缓冲区创建时为空,因此连接成功时,就触发可写事件
  56. # 这时再转为监听可读事件,接收到数据时,就可以触发可读事件了
  57. writeable.remove(sock)
  58. readable.append(sock)
  59. sock_to_crawler[sock].next_step()
  60. for sock in rs:
  61. try:
  62. sock_to_crawler[sock].next_step()
  63. except StopIteration:
  64. # 如果生成器结束了,就说明对应的爬虫任务已经结束,不需要监听事件了
  65. readable.remove(sock)
  66. # 所有的事件都结束后,就退出循环
  67. if not readable and not writeable:
  68. break
  69.  
  70. if __name__ == '__main__':
  71. start = time.time()
  72. n = 10
  73. print('开始爬取...')
  74. event_loop([GenCrawler() for _ in range(n)])
  75. print('获取到{}条数据,用时{:.2f}秒'.format(len(db), time.time()-start))

这段代码使用IO多路复用对多个socket进行监听, 监听到事件时, 驱动对应的生成器运行, 运行到IO操作时, 再使用yield切换回事件循环, 从而实现并发的效果, 这个也就是asyncio中事件循环的工作原理.

  由于asyncio中的事件循环使用的是selectors模块而非select, 现在在程序的代码中改用selectors模块:

  1. import socket
  2. import time
  3. from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
  4.  
  5. req = 'GET / HTTP/1.0\r\nHost:cn.bing.com\r\n\r\n'.encode('utf8')
  6. address = ('cn.bing.com', 80)
  7. db = []
  8.  
  9. class EventLoop:
  10.  
  11. def __init__(self):
  12. self.selector = DefaultSelector()
  13. self._stopped = False
  14.  
  15. def register(self, fd, event, callback):
  16. self.selector.register(fd, event, callback)
  17.  
  18. def unregister(self, fd):
  19. self.selector.unregister(fd)
  20.  
  21. def run_until_complete(self,gens):
  22. for gen in gens:
  23. next(gen)
  24. while not self._stopped:
  25. try:
  26. events = self.selector.select()
  27. except OSError:
  28. # 如果当前没有注册事件, 就会引发OSError异常
  29. continue
  30. for key, mask in events:
  31. # 这里的callback就是注册事件时传入的回调函数
  32. callback = key.data
  33. callback(key=key, mask=mask)
  34.  
  35. # 生成器的gi_frame属性对应的是其框架(其实这属性我还没搞懂)
  36. # 在生成器结束(抛出stopiteration异常)后,这个属性值就会变成None
  37. # 因此,每次循环时都删减已经结束的生成器
  38. # 如果所有的生成器都结束了,就停止循环
  39. gens = [gen for gen in gens if gen.gi_frame is not None]
  40. if not gens:
  41. self.stop()
  42.  
  43. def stop(self):
  44. self._stopped = True
  45. self.selector.close()
  46.  
  47. loop = EventLoop()
  48.  
  49. class GenCrawler:
  50.  
  51. def __init__(self):
  52. self.sock = socket.socket()
  53. self.sock.setblocking(0)
  54. self._fd = self.sock.fileno()
  55. self.gen = self._crawler()
  56.  
  57. def _crawler(self):
  58. self.sock.connect_ex(address)
  59. loop.register(self._fd, EVENT_WRITE, self.next_step)
  60. yield
  61. loop.unregister(self._fd)
  62. self.sock.send(req)
  63. response = b''
  64. while 1:
  65. loop.register(self._fd, EVENT_READ, self.next_step)
  66. yield
  67. loop.unregister(self._fd)
  68. chunk = self.sock.recv(1024)
  69. if chunk == b'':
  70. self.sock.close()
  71. break
  72. else:
  73. response += chunk
  74. db.append(response)def next_step(self,**kwargs):
  75. try:
  76. next(self.gen)
  77. except StopIteration:
  78. return
  79.  
  80. if __name__ == '__main__':
  81. start = time.time()
  82. print('开始爬取...')
  83. n = 10
  84. gens = [GenCrawler().gen for _ in range(n)]
  85. loop.run_until_complete(gens)
  86. print('获取到{}条数据,用时{:.2f}秒'.format(len(db), time.time()-start))

这里主要是改了EventLoop部分的代码, 使用register和unregister方法来注册和注销事件, 优点是更加灵活, 可以指定触发事件时调用的回调函数. 另外, DefaultSelector会自动选择系统中效率最高的多路复用机制, 比如kqueue和epoll.

2. async与协程

  在定义函数的时候, 在def之前加上async, 这个函数就不是普通函数了, 而是一个协程函数:

  1. async def coro():
  2. print('this is a coroutine')

直接调用协程函数并不能使之运行, 而是返回了一个协程对象, 如果要运行该协程, 可以调用这个协程对象的send方法:

  1. c=coro()
  2. c.send(None)

运行结果如下, 首先会运行协程函数内部的代码, 然后函数的代码运行结束, 抛出一个StopIteration异常:

  因此, 协程函数与生成器函数是非常相似的. 但是, 协程不是可迭代对象, 因此无法使用next函数, 只能调用其自身的send方法来驱动.

  1. python3.6开始, 协程函数中可以使用yield语句, 此时调用这个函数, 就会返回一个async_generator对象, 即异步生成器.
  2. 不过这东西我还没用过, 先挖个坑, 需要的可以看PEP525.

补充说明

3. await和awaitable

  在第一节中讲到, 协程中可以使用await语句, 后接awaitable对象, 即可等待对象. 以下几类都是可等待对象:

  • 一个协程, 这个上一小节刚讲.
  • 一个有__await__方法, 并且该方法返回一个迭代器的对象, 常见情况是这个对象的__await__方法就是个生成器函数.
  • 使用@types.coroutine装饰的生成器函数, 其中types是python内置的一个库, 这个装饰器的实现原理是返回一个定义了__await__方法的对象.
  • Objects defined with CPython C API with a tp_as_async.am_await function, returning an iterator (similar to __await__ method). 这一条是从官网抄的, 我没理解, 应该和定义__await__类似吧.

现在定义一个可等待对象并测试:

  1. class AwaitableObj:
  2.  
  3. def __await__(self):
  4. v = yield '来自可等待对象的yield'
  5. print('可等待对象获得的值:', v)
  6. return '来自可等待对象的return'
  7.  
  8. async def coro():
  9. v = await AwaitableObj()
  10. print('协程获得的值:', v)
  11.  
  12. if __name__ == '__main__':
  13. c = coro()
  14. v = c.send(None)
  15. print('外部获得的值:', v)
  16. try:
  17. c.send('来自外部')
  18. except StopIteration:
  19. pass

这段程序有三个部分: 可等待对象, 协程和外部. 协程中使用await语句来等待可等待对象, 而外部调用send方法来驱动协程.

程序的运行结果如下:

await相当于外部与可等待对象之间的桥梁, 可等待对象中__await__方法返回的生成器, 其yield返回的值会传到外部, 而外部使用send方法传的值也会传给可等待对象的生成器. 最后__await__生成器迭代结束后, 协程获得其返回值.

  这里需要说明一点: await语句本身并不能暂停和切换协程, 它只是阻塞协程直到后面接的可等待对象的__await__方法返回的可迭代对象运行完. 如果__await__里面有yield, 返回一个生成器, 协程才会因为这个yield语句暂停和切换.

4. future/task

  future是asyncio模块中的一个可等待对象, 调用asyncio.get_event_loop获取到当前线程的事件循环loop, 然后调用loop.create_future, 就可以得到一个future对象. future的主要代码如下(有改动):

  1. class Future:
  2.  
  3. def __init__(self):
  4. self._callbacks = []
  5. self.result = None
  6.  
  7. def add_callback(self, callback):
  8. self._callbacks.append(callback)
  9.  
  10. def set_result(self, result):
  11. self.result = result
  12. for callback in self._callbacks:
  13. callback(self)
  14.  
  15. def __await__(self):
  16. yield self
  17. return self.result

  future可以理解为协程的一次暂停. 首先, 如果一个协程需要在某处暂停, 就可以实例化一个future对象并且await这个对象, 这样就会运行future对象的__await__方法, 当运行到yield self这句话时, 协程暂停, 直到外部再使用send方法驱动协程为止. 然后, future的另一特性是可以设置回调函数, 调用它的add_callback方法就行. 最后, future还有set_result这个接口, 一方面会运行future的回调函数, 另一方面可以设置其result属性的值, 该值在__awaiit__方法结束之后返回给协程. 一般的用法是, 协程在事件循环中注册事件, 然后让事件循环来调用future对象的set_result方法.

  有了暂停, 自然也需要有驱动, task对象负责对协程进行封装和驱动. 调用asyncio.create_task并传入协程对象, 就可以得到一个task对象. task的主要代码如下(有改动):

  1. class Task(Future):
  2.  
  3. def __init__(self, coro):
  4. super().__init__()
  5. self.coro = coro
  6. f = Future()
  7. f.set_result(None)
  8. self.step(f)
  9.  
  10. def step(self, future):
  11. try:
  12. next_future = self.coro.send(None)
  13. except StopIteration:
  14. self.set_result(future.result)
  15. return
  16. next_future.add_callback(self.step)

  task和future应该是搭配使用的. 首先, task.step是负责对协程进行驱动的, 由于future.__await__方法会yield self, 因此每次驱动都会获得目前暂停点对应的future对象. 这时候将自己的step方法添加到future对象的回调中, 等到future对象调用set_result方法时, 就会回调到task.step方法, 从而驱动协程继续运行. 因此可以认为, future对象就是协程的一次暂停, 而调用其set_result方法就意味着这次暂停结束了, 但是这个过程需要task的协助.

  task类是继承future类的, 这其实比较好理解, 比如一个简单的爬虫任务, 在连接服务器和接受数据等IO操作时需要使用future暂停, 并可以设置回调, 表示暂停结束的时候应该做什么. 而这个爬虫任务相当于一个大的IO操作, 因此也应该有可以设置回调以及可以await的特性. 当一个协程驱动结束, 即抛出StopIteration异常的时候, 就意味着这个task结束了, 因此此时就调用task.set_result方法, 把最后一个future对象的结果设置为task.result.

5. 爬虫代码重构

  把上面讲的async/await, future/task等内容添加到之前的爬虫实例中, 最终代码如下:

  1. import socket
  2. import time
  3. from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
  4.  
  5. req = 'GET / HTTP/1.0\r\nHost:cn.bing.com\r\n\r\n'.encode('utf8')
  6. address = ('cn.bing.com', 80)
  7. db = []
  8.  
  9. class EventLoop:
  10.  
  11. def __init__(self):
  12. self.selector = DefaultSelector()
  13. self._stopped = False
  14.  
  15. def register(self, fd, event, callback):
  16. self.selector.register(fd, event, callback)
  17.  
  18. def unregister(self, fd):
  19. self.selector.unregister(fd)
  20.  
  21. def run_until_complete(self, coros):
  22. def _done_callback(fut):
  23. nonlocal ncoros
  24. ncoros -= 1
  25. if ncoros == 0:
  26. self.stop()
  27. ncoros = len(coros)
  28. for coro in coros:
  29. task = Task(coro)
  30. task.add_callback(_done_callback)
  31.  
  32. while not self._stopped:
  33. try:
  34. events = self.selector.select()
  35. except OSError:
  36. # 如果当前没有注册事件, 就会引发OSError异常
  37. continue
  38. for key, mask in events:
  39. # 这里的callback就是注册事件时传入的回调函数
  40. callback = key.data
  41. callback(key=key, mask=mask)
  42.  
  43. def stop(self):
  44. self._stopped = True
  45. self.selector.close()
  46.  
  47. loop = EventLoop()
  48.  
  49. class Future:
  50.  
  51. def __init__(self):
  52. self._callbacks = []
  53. self.result = None
  54.  
  55. def add_callback(self, callback):
  56. self._callbacks.append(callback)
  57.  
  58. def set_result(self, result):
  59. self.result = result
  60. for callback in self._callbacks:
  61. callback(self)
  62.  
  63. def __await__(self):
  64. yield self
  65. return self.result
  66.  
  67. class Task(Future):
  68.  
  69. def __init__(self, coro):
  70. super().__init__()
  71. self.coro = coro
  72. f = Future()
  73. f.set_result(None)
  74. self.step(f)
  75.  
  76. def step(self, future):
  77. try:
  78. next_future = self.coro.send(None)
  79. except StopIteration:
  80. self.set_result(future.result)
  81. return
  82. next_future.add_callback(self.step)
  83.  
  84. class CoroCrawler:
  85.  
  86. def __init__(self):
  87. self.sock = socket.socket()
  88. self.sock.setblocking(0)
  89. self._fd = self.sock.fileno()
  90. self.coro = self._crawler()
  91.  
  92. async def _crawler(self):
  93. await self.connect()
  94. self.sock.send(req)
  95. response = await self.read_all()
  96. db.append(response)
  97.  
  98. async def connect(self):
  99. self.sock.connect_ex(address)
  100.  
  101. f = Future()
  102.  
  103. def on_connect(key, mask):
  104. f.set_result(None)
  105. loop.register(self.sock.fileno(), EVENT_WRITE, on_connect)
  106. await f
  107. loop.unregister(self.sock.fileno())
  108.  
  109. async def read_all(self):
  110. response = b''
  111. while 1:
  112. chunk = await self.read()
  113. if chunk == b'':
  114. self.sock.close()
  115. break
  116. response += chunk
  117. return response
  118.  
  119. async def read(self):
  120. f = Future()
  121.  
  122. def on_readable(key, mask):
  123. chunk = self.sock.recv(1024)
  124. f.set_result(chunk)
  125. loop.register(self._fd, EVENT_READ, on_readable)
  126. chunk = await f
  127. loop.unregister(self._fd)
  128. return chunk
  129.  
  130. if __name__ == '__main__':
  131. start = time.time()
  132. print('开始爬取...')
  133. n = 10
  134. coros = [CoroCrawler().coro for _ in range(n)]
  135. loop.run_until_complete(coros)
  136. print('获取到{}条数据,用时{:.2f}秒'.format(len(db), time.time()-start))

  这段代码并不算复杂, 唯一需要留意的就是事件循环中的run_until_complete方法, 这个方法不再是主动去检查任务是否结束, 而是将协程包装成task对象, 然后给task对象添加了回调函数, 来在协程全部结束时, 停止事件循环. 这也就是用task包装协程的一个方便的地方: 可以在协程结束的时候运行指定的回调.

  整个代码的实现流程如下, 这也就是用asyncio运行一个协程的流程.

三. 总结

  • asyncio模块是基于async/await实现的一个异步库, 用法的话, 简单来说就是首先定义好协程, 然后把协程打包扔给asyncio模块, 最后启动事件循环, 就实现异步了.
  • asyncio底层使用事件循环, 其本质是系统的IO多路复用机制, 通过这种机制来同时监听多个对象, 在触发事件时调用对应的回调函数, 从而实现异步和并发的效果.
  • Future表示协程单个断点的运行状态, Task继承自Future, 表示整个协程的运行状态, 二者都可以设置回调函数, 在结束的时候调用回调.
  • Task是对协程对象的封装和管理, 负责驱动协程, Future则直接对接loop循环, 接收回调函数的结果并返回.
  • 要定义协程, 首先要使用async定义函数, 然后如果有耗时操作, 在耗时操作前面加上await. 不过, 对应的耗时操作必须是awaitable的对象.

从零开始学asyncio(中)的更多相关文章

  1. 从零开始学asyncio(上)

    这篇文章主要是介绍生成器和IO多路复用机制, 算是学习asyncio需要的预备知识. 这个系列还有另外两篇文章: 从零开始学asyncio(中) 从零开始学asyncio(下) 一. 简单爬虫实例 首 ...

  2. 从零开始学 Java - log4j 项目中的详细配置

    你还会用笔来写字么 我是不怎么会了,有时候老是拿起笔之后不知道这个字怎么写,这时候就会拿起手机去打出来:有时候还会写出来这个字之后越看越不像,这时候就开始怀疑自己的能力了:有时候写出来了一大堆字之后, ...

  3. 从零开始学 Web 之 Ajax(六)jQuery中的Ajax

    大家好,这里是「 从零开始学 Web 系列教程 」,并在下列地址同步更新...... github:https://github.com/Daotin/Web 微信公众号:Web前端之巅 博客园:ht ...

  4. 56. spring boot中使用@Async实现异步调用【从零开始学Spring Boot】

    什么是"异步调用"? "异步调用"对应的是"同步调用",同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执 ...

  5. (41)Spring Boot 使用Java代码创建Bean并注册到Spring中【从零开始学Spring Boot】

    已经好久没有讲一些基础的知识了,这一小节来点简单的,这也是为下节的在Spring Boot中使用多数据源做准备. 从Spring 3.0开始,增加了一种新的途径来配置Bean Definition,这 ...

  6. 从零开始学Java——个人笔记(持续更新中)

    从零开始学Java 学习流程 第一阶段:建立编程思想 Java概述 变量 运算符 控制结构 数组.排序和查找 面向对象编程(基础) 面向对象编程(中级) 项目&学习以致用 编程之乐 第二阶段: ...

  7. 从零开始学 Java - Spring 集成 Memcached 缓存配置(二)

    Memcached 客户端选择 上一篇文章 从零开始学 Java - Spring 集成 Memcached 缓存配置(一)中我们讲到这篇要谈客户端的选择,在 Java 中一般常用的有三个: Memc ...

  8. 从零开始学 Java - Spring 集成 ActiveMQ 配置(一)

    你家小区下面有没有快递柜 近两年来,我们收取快递的方式好像变了,变得我们其实并不需要见到快递小哥也能拿到自己的快递了.对,我说的就是类似快递柜.菜鸟驿站这类的代收点的出现,把我们原来快递小哥必须拿着快 ...

  9. 从零开始学 Java - Spring 集成 Memcached 缓存配置(一)

    硬盘和内存的作用是什么 硬盘的作用毫无疑问我们大家都清楚,不就是用来存储数据文件的么?如照片.视频.各种文档或等等,肯定也有你喜欢的某位岛国老师的动作片,这个时候无论我们电脑是否关机重启它们永远在那里 ...

随机推荐

  1. 异常解决:non-compatible bean definition of same name and class【com.xxx.xxx.XXX】

    昨天同事遇到这样一个问题,意思是spring找到 有相同的实现类名在不同的package目录下. 跟踪他的项目代码并未发现问题.   重新给他的maven项目进行maven install一下. 查看 ...

  2. behavior planning——inputs to transition functions

    the answer is that we have to pass all  of the data into transition function except for the previous ...

  3. js写出你的名字的拼音,判断哪个字母出现的最多

    function fn(str) { var obj = {}; for (var i = 0; i < str.length; i++) { if (!obj[str.charAt(i)]) ...

  4. HTML静态网页--JavaScript-语法

    1.基本数据类型: 字符串.小数.整数.日期时间.布尔型等. 2.变量: 都是通用类型var,可以随便存储其他类型的值,可以直接使用,不用定义,但习惯上定义.定义变量:var a:所有变量定义 都用v ...

  5. axis2 wsdl2java工具

    wsdl2java工具使用方法描述: C:\Users\Administrator>wsdl2java -h Using AXIS2_HOME: E:\Apache_Projects\axis2 ...

  6. Laravel修改配置后一定要清理缓存 "php artisan config:clear"!

    用laravel踩到一个大坑... 需要使用laravel的队列(queue)功能, 设置 ".env"配置文件 QUEUE_DRIVER=database 按照文档,建立jobs ...

  7. 详解PhpStudy集成环境升级MySQL数据库版本

    http://phpstudy.php.cn/jishu-php-2967.html phpstudy里没有地方可以设置mysql数据库,很多人都疑惑在phpstudy里怎么升级mysql数据库版本, ...

  8. poj 3279(开关问题)(待完成)

    传送门:Problem 3279 #include<iostream> #include<cstdio> #include<cstring> using names ...

  9. python模块之包

    包:将解决一类问题的模块放在同一目录下就形成了一个包 为了更好的了解包,我们就模拟创建一个包 import os os.makedirs('glance/api') os.makedirs('glan ...

  10. JavaMail转发邮件

    最近要做一个邮件转发功能,看了好多blog,都是接受邮件,再解析邮件内容,再组装成新的邮件发出! 我按照这个不够,不错!邮件发出去了.但是好麻烦啊,接受邮件是个Message,发送邮件也是个Messa ...