本文代码整理自:深入理解Python异步编程(上)

参考:A Web Crawler With asyncio Coroutines

一、同步阻塞方式

import socket

def blocking_way():
sock = socket.socket()
# blocking
sock.connect(('example.com', 80))
request = 'GET / HTTP/1.0\r\nHost: example.com\r\n\r\n'
sock.send(request.encode('ascii'))
response = b''
chunk = sock.recv(4096)
while chunk:
response += chunk
# blocking
chunk = sock.recv(4096)
return response def sync_way():
res = []
for i in range(10):
res.append(blocking_way())
return len(res) def main():
start = time.time()
print(sync_way())
print(time.time() - start) if __name__ == '__main__':
import time
main() # 5.15s

二、同步多线程方式

import socket
from concurrent import futures def blocking_way():
sock = socket.socket()
# blocking
sock.connect(('example.com', 80))
request = 'GET / HTTP/1.0\r\nHost: example.com\r\n\r\n'
sock.send(request.encode('ascii'))
response = b''
chunk = sock.recv(4096)
while chunk:
response += chunk
# blocking
chunk = sock.recv(4096)
return response def thread_way():
workers = 10
with futures.ThreadPoolExecutor(workers) as executor:
futs = {executor.submit(blocking_way) for i in range(10)}
return len([fut.result() for fut in futs]) def main():
start = time.time()
print(thread_way())
print(time.time() - start) if __name__ == '__main__':
import time
main() # 0.52s

  

小提示

Python中的多线程因为GIL的存在,它们并不能利用CPU多核优势,
一个Python进程中,只允许有一个线程处于运行状态。 那为什么结果还是如预期,耗时缩减到了十分之一? 因为在做阻塞的系统调用时,例如sock.connect(),sock.recv()时,当前线程会释放GIL,
让别的线程有执行机会。但是单个线程内,在阻塞调用上还是阻塞的 Python中 time.sleep 是阻塞的,都知道使用它要谨慎,
但在多线程编程中,time.sleep 并不会阻塞其他线程。

  

三、非阻塞+回调(即异步非阻塞)方式

事件循环+回调     实现单线程内异步编程

事件监听

OS将I/O状态的变化都封装成了事件,如可读事件、可写事件。
并且提供了专门的系统模块让应用程序可以接收事件通知。这个模块就是select。
让应用程序可以通过select注册文件描述符和回调函数。
当文件描述符的状态发生变化时,select 就调用事先注册的回调函数。 select因其算法效率比较低,后来改进成了poll;
再后来又有进一步改进,BSD内核改进成了kqueue模块,而Linux内核改进成了epoll模块。这四个模块的作用都相同,暴露给程序员使用的API也几乎一致,
区别在于kqueue 和 epoll 在处理大量文件描述符时效率更高。

selectors模块

Python标准库提供的selectors模块是对底层select/poll/epoll/kqueue的封装。
DefaultSelector类会根据 OS 环境自动选择最佳的模块,
那在 Linux 2.5.44 及更新的版本上都是epoll了。

  

#!/usr/bin/python3.5
# encoding: utf-8 import socket
from selectors import DefaultSelector, EVENT_WRITE, EVENT_READ selector = DefaultSelector()
stopped = False
urls_todo = {'/', '/1', '/2', '/3', '/4', '/5', '/6', '/7', '/8', '/9'} class Crawler:
def __init__(self, url):
self.url = url
self.sock = None
self.response = b'' def fetch(self):
self.sock = socket.socket()
self.sock.setblocking(False)
try:
self.sock.connect(('example.com', 80))
except BlockingIOError:
pass
selector.register(self.sock.fileno(), EVENT_WRITE, self.connected) def connected(self, key, mask):
selector.unregister(key.fd)
get = 'GET {0} HTTP/1.0\r\nHost: example.com\r\n\r\n'.format(self.url)
self.sock.send(get.encode('ascii'))
selector.register(key.fd, EVENT_READ, self.read_response) def read_response(self, key, mask):
global stopped
# 如果响应大于4KB,下一次循环会继续读
chunk = self.sock.recv(4096)
if chunk:
self.response += chunk
else:
selector.unregister(key.fd)
urls_todo.remove(self.url)
if not urls_todo:
stopped = True # 事件循环
def loop():
while not stopped:
# 阻塞, 直到一个事件发生
events = selector.select()
for event_key, event_mask in events:
callback = event_key.data
callback(event_key, event_mask) if __name__ == '__main__':
import time
start = time.time()
for url in urls_todo:
crawler = Crawler(url)
crawler.fetch()
loop()
print(time.time() - start) # 0.53s

回调层次过多的缺点:

- 共享状态管理困难

在回调的版本中,我们必须在Crawler实例化后的对象self里保存它自己的sock对象。

如果不是采用OOP的编程风格,那需要把要共享的状态接力似的传递给每一个回调。

多个异步调用之间,到底要共享哪些状态,事先就得考虑清楚,精心设计。

- 错误处理困难

一连串的回调构成一个完整的调用链;
如果其中一环抛了异常怎么办?
整个调用链断掉,接力传递的状态也会丢失,这种现象称为调用栈撕裂。 所以,为了防止栈撕裂,异常必须以数据的形式返回,而不是直接抛出异常,
然后每个回调中需要检查上次调用的返回值,以防错误吞没。

四、Python 对异步I/O的优化之路

#!/usr/bin/python3.5
# encoding: utf-8 import socket
from selectors import DefaultSelector, EVENT_WRITE, EVENT_READ selector = DefaultSelector()
stopped = False
urls_todo = {'/', '/1', '/2', '/3', '/4', '/5', '/6', '/7', '/8', '/9'} class Future:
def __init__(self):
self.result = None
self._callbacks = [] def add_done_callback(self, fn):
self._callbacks.append(fn) def set_result(self, result):
self.result = result
for fn in self._callbacks:
fn(self) class Crawler:
def __init__(self, url):
self.url = url
self.response = b'' def fetch(self):
sock = socket.socket()
sock.setblocking(False)
try:
sock.connect(('example.com', 80))
except BlockingIOError:
pass
f = Future() def on_connected():
f.set_result(None) selector.register(sock.fileno(), EVENT_WRITE, on_connected)
yield f
selector.unregister(sock.fileno())
get = 'GET {0} HTTP/1.0\r\nHost: example.com\r\n\r\n'.format(self.url)
sock.send(get.encode('ascii')) global stopped
while True:
f = Future() def on_readable():
f.set_result(sock.recv(4096)) selector.register(sock.fileno(), EVENT_READ, on_readable)
chunk = yield f
selector.unregister(sock.fileno())
if chunk:
self.response += chunk
else:
urls_todo.remove(self.url)
if not urls_todo:
stopped = True
break class Task:
def __init__(self, coro):
self.coro = coro
f = Future()
f.set_result(None)
self.step(f) def step(self, future):
try:
# send会进入到coro执行, 即fetch, 直到下次yield
# next_future 为yield返回的对象
next_future = self.coro.send(future.result)
except StopIteration:
return
next_future.add_done_callback(self.step) # 事件循环
def loop():
while not stopped:
# 阻塞, 直到一个事件发生
events = selector.select()
for event_key, event_mask in events:
callback = event_key.data
callback() if __name__ == '__main__':
import time
start = time.time()
for url in urls_todo:
crawler = Crawler(url)
Task(crawler.fetch())
loop()
print(time.time() - start) # 0.53s

在前辈的基础上做了一点更改:

#!/usr/bin/python3
# encoding: utf-8 import socket
from selectors import DefaultSelector, EVENT_WRITE, EVENT_READ selector = DefaultSelector()
stopped = False
urls_todo = {'/', '/1', '/2', '/3', '/4', '/5', '/6', '/7', '/8', '/9'} class Future:
def __init__(self):
self.result = None
self._callback = None # 原来是用列表来保存 def add_done_callback(self, fn):
self._callback = fn def set_result(self, result):
self.result = result
# 因为只有一个对应的 Task.step()函数
if self._callback:
self._callback(self) class Crawler:
def __init__(self, url):
self.url = url
self.response = b'' def fetch(self):
sock = socket.socket()
sock.setblocking(False)
try:
sock.connect(('example.com', 80))
except BlockingIOError:
pass
f = Future() def on_connected():
f.set_result(None) selector.register(sock.fileno(), EVENT_WRITE, on_connected)
yield f
selector.unregister(sock.fileno())
get = 'GET {0} HTTP/1.0\r\nHost: example.com\r\n\r\n'.format(self.url)
sock.send(get.encode('ascii')) global stopped
while True:
f = Future() def on_readable():
f.set_result(sock.recv(4096)) selector.register(sock.fileno(), EVENT_READ, on_readable)
chunk = yield f
selector.unregister(sock.fileno())
if chunk:
self.response += chunk
else:
urls_todo.remove(self.url)
if not urls_todo:
stopped = True
break class Task:
def __init__(self, coro):
self.coro = coro
f = Future()
f.set_result(None)
self.step(f) def step(self, future):
try:
# send会进入到coro执行, 即fetch, 直到下次yield
# next_future 为yield返回的对象
next_future = self.coro.send(future.result)
except StopIteration:
return
next_future.add_done_callback(self.step)
print(next_future._callback) # 事件循环
def loop():
while not stopped:
# 阻塞, 直到一个事件发生
events = selector.select()
for event_key, event_mask in events:
callback = event_key.data
callback() if __name__ == '__main__':
import time
start = time.time()
c_list = []
for url in urls_todo:
crawler = Crawler(url)
Task(crawler.fetch())
c_list.append(crawler) loop()
# 增加了对爬取内容的输出
for crawler in c_list:
print(crawler.response)
print(time.time() - start)

  

五、用 yield from 改进生成器协程

yield可以直接作用于普通Python对象,而yield from却不行,

所以我们对Future还要进一步改造,把它变成一个iterable对象就可以了

#!/usr/bin/python3.5
# -*- coding:utf-8 -*- import socket
from selectors import DefaultSelector, EVENT_WRITE, EVENT_READ selector = DefaultSelector()
stopped = False
urls_todo = {'/', '/1', '/2', '/3', '/4', '/5', '/6', '/7', '/8', '/9'} def connect(sock, address):
f = Future()
sock.setblocking(False)
try:
sock.connect(address)
except BlockingIOError:
pass def on_connected():
f.set_result(None) selector.register(sock.fileno(), EVENT_WRITE, on_connected)
yield from f
selector.unregister(sock.fileno()) def read(sock):
f = Future() def on_readable():
f.set_result(sock.recv(4096)) selector.register(sock.fileno(), EVENT_READ, on_readable)
chunk = yield from f
selector.unregister(sock.fileno())
return chunk def read_all(sock):
response = []
chunk = yield from read(sock)
while chunk:
response.append(chunk)
chunk = yield from read(sock)
return b''.join(response) class Future:
def __init__(self):
self.result = None
self._callbacks = [] def add_done_callback(self, fn):
self._callbacks.append(fn) def set_result(self, result):
self.result = result
for fn in self._callbacks:
fn(self) def __iter__(self):
yield self
return self.result class Crawler:
def __init__(self, url):
self.url = url
self.response = b'' def fetch(self):
global stopped
sock = socket.socket()
yield from connect(sock, ('example.com', 80))
get = 'GET {0} HTTP/1.0\r\nHost: example.com\r\n\r\n'.format(self.url)
sock.send(get.encode('ascii'))
self.response = yield from read_all(sock)
urls_todo.remove(self.url)
if not urls_todo:
stopped = True class Task:
def __init__(self, coro):
self.coro = coro
f = Future()
f.set_result(None)
self.step(f) def step(self, future):
try:
# send会进入到coro执行, 即fetch, 直到下次yield
# next_future 为yield返回的对象
next_future = self.coro.send(future.result)
except StopIteration:
return
next_future.add_done_callback(self.step) # 事件循环
def loop():
while not stopped:
# 阻塞, 直到一个事件发生
events = selector.select()
for event_key, event_mask in events:
callback = event_key.data
callback() if __name__ == '__main__':
import time
start = time.time()
for url in urls_todo:
crawler = Crawler(url)
Task(crawler.fetch())
loop()
print(time.time() - start) # 0.53s

  

六、asyncio和原生协程初体验

asyncio是Python 3.4 试验性引入的异步I/O框架(PEP 3156),提供了基于协程做异步I/O编写单线程并发代码的基础设施。

其核心组件有事件循环(Event Loop)、协程(Coroutine)、任务(Task)、未来对象(Future)以及其他一些扩充和辅助性质的模块。

在引入asyncio的时候,还提供了一个装饰器@asyncio.coroutine用于装饰使用了yield from的函数,以标记其为协程。但并不强制使用这个装饰器。

在 3.5 中新增了async/await语法(PEP 492),对协程有了明确而显式的支持,称之为原生协程

async/await 和 yield from这两种风格的协程底层复用共同的实现,而且相互兼容。

在Python 3.6 中asyncio库“转正”,不再是实验性质的,成为标准库的正式一员。

#!/usr/bin/python3.5
# -*- coding:utf-8 -*- import asyncio
import aiohttp host = 'http://example.com'
urls_todo = {'/', '/1', '/2', '/3', '/4', '/5', '/6', '/7', '/8', '/9'} loop = asyncio.get_event_loop() async def fetch(url):
async with aiohttp.ClientSession(loop=loop) as session:
async with session.get(url) as response:
response = await response.read()
return response if __name__ == '__main__':
import time
start = time.time()
tasks = [fetch(host + url) for url in urls_todo]
loop.run_until_complete(asyncio.gather(*tasks))
print(time.time() - start) # 0.54s

2019-06-26补充demo示例

 import time
import asyncio
import requests urls = [
'http://httpbin.org/get',
'http://httpbin.org/ip',
'http://httpbin.org/json',
'http://httpbin.org/uuid',
'http://httpbin.org/user-agent',
'http://httpbin.org/headers',
'http://httpbin.org/response-headers',
] def get_result(url):
d = requests.get(url)
dd = d.json()
return dd start = time.time() results = []
for url in urls:
d = get_result(url)
results.append(d) print('RUN : {}'.format(time.time()-start))
print(results)

耗时:RUN : 6.703306198120117

 import time
import asyncio
import requests urls = [
'http://httpbin.org/get',
'http://httpbin.org/ip',
'http://httpbin.org/json',
'http://httpbin.org/uuid',
'http://httpbin.org/user-agent',
'http://httpbin.org/headers',
'http://httpbin.org/response-headers',
] def myfunc(url):
d = requests.get(url)
dd = d.json()
return dd @asyncio.coroutine
def fetch_async(func, url):
loop = asyncio.get_event_loop()
future = loop.run_in_executor(None, func, url)
data = yield from future
return data start = time.time()
loop = asyncio.get_event_loop()
tasks = [fetch_async(myfunc, url) for url in urls]
results = loop.run_until_complete(asyncio.gather(*tasks))
loop.close() print('RUN : {}'.format(time.time()-start))
print(results)

耗时:RUN : 1.0276665687561035

补充说明

run_in_executor(self, executor, func, *args) 第一个参数是传入一个executor(即concurrent.futures.ThreadPoolExecutor,线程池对象),
不传的话,默认使用 (os.cpu_count() or 1) * 5 这个数值,即如果是4核的cpu,就会对应生成一个含有20线程的线程池,来执行传入的第二个函数func.
所以run_in_executor其实开启了新的线程,再协调各个线程

  

深入理解Python异步编程(上)的更多相关文章

  1. 深入理解 Python 异步编程(上)

    http://python.jobbole.com/88291/ 前言 很多朋友对异步编程都处于"听说很强大"的认知状态.鲜有在生产项目中使用它.而使用它的同学,则大多数都停留在知 ...

  2. 这篇文章讲得精彩-深入理解 Python 异步编程(上)!

    可惜,二和三现在还没有出来~ ~~~~~~~~~~~~~~~~~~~~~~~~~ http://python.jobbole.com/88291/ ~~~~~~~~~~~~~~~~~~~~~~~~~~ ...

  3. 快速理解Python异步编程的基本原理

    第一个例子 假设你需要用电饭煲煮饭,用洗衣机洗衣服,给朋友打电话让他过来吃饭.其中,电饭煲需要30分钟才能把饭煮好,洗衣机需要40分钟才能把衣服洗好,朋友需要50分钟才能到你家.那么,是不是你需要在这 ...

  4. python 异步编程

    Python 3.5 协程究竟是个啥 Yushneng · Mar 10th, 2016 原文链接 : How the heck does async/await work in Python 3.5 ...

  5. Python 异步编程笔记:asyncio

    个人笔记,不保证正确. 虽然说看到很多人不看好 asyncio,但是这个东西还是必须学的.. 基于协程的异步,在很多语言中都有,学会了 Python 的,就一通百通. 一.生成器 generator ...

  6. 最新Python异步编程详解

    我们都知道对于I/O相关的程序来说,异步编程可以大幅度的提高系统的吞吐量,因为在某个I/O操作的读写过程中,系统可以先去处理其它的操作(通常是其它的I/O操作),那么Python中是如何实现异步编程的 ...

  7. (转)python异步编程--回调模型(selectors模块)

    原文:https://www.cnblogs.com/zzzlw/p/9384308.html#top 目录 0. 参考地址 1. 前言 2. 核心类 3. SelectSelector核心函数代码分 ...

  8. python异步编程--回调模型(selectors模块)

    目录 0. 参考地址 1. 前言 2. 核心类 3. SelectSelector核心函数代码分析 3.1 注册 3.2 注销 3.3 查询 4. 别名 5. 总结 6. 代码报错问题 1. 文件描述 ...

  9. python异步编程 (转载)

    Python Async/Await入门指南   转自:https://zhuanlan.zhihu.com/p/27258289 本文将会讲述Python 3.5之后出现的async/await的使 ...

随机推荐

  1. [luogu1486][bzoj1503][NOI2004]郁闷的出纳员【平衡树treap】

    题目描述 OIER公司是一家大型专业化软件公司,有着数以万计的员工.作为一名出纳员,我的任务之一便是统计每位员工的工资.这本来是一份不错的工作,但是令人郁闷的是,我们的老板反复无常,经常调整员工的工资 ...

  2. CPU监控 解题报告

    CPU监控 这种题就需要小黄鸭调试法,不行就重构,动态gdb可能会死人,一堆tag的... 维护历史最值的一个核心是历史最值tag,它的意义是从上一次这个点下放tag之后到当前时刻的这个点的tag达到 ...

  3. bzoj3756pty的字符串(后缀自动机+计数)

    题目描述 题解 我们可以先对trie树建出广义SAM,然后维护一下right集合大小(注意right集合在广义SAM上的维护方式). 然后把匹配穿往广义SAM上匹配,假设现在匹配到了x节点,那么x的所 ...

  4. 搭建简易的WebServer(基于pyhton实现简易Web框架 使用socket套接字)

    1. 使用web底层socket的方式实现简易服务器的搭建,用来理解学习 # 1.导入socket模块 import socket import re import gevent import sys ...

  5. Surface Pro 4 系统优化全教程及QA

    甲.如何根据你的需求选择相应版本的surface1.Core M3-6Y30,Core i5-6300U,Core i7-6650U的性能差距有多大?2.HD 515,HD520,Iris 540的性 ...

  6. PMP认证考试的最新趋势及10大特征(针对改版后)

    我们都知道,今年PMP认证考试的教材已经改版了,最新版的内容是有不少的改动的,我们在了解PMP考试的时候,也要了解PMP考试的最新趋势,以便拿出应对的方法. 一.情景题更接地气 虽然PMP考试中继续保 ...

  7. 自定义QMenu

    参考: http://blog.csdn.net/qq1623803207/article/details/77449884 http://blog.sina.com.cn/s/blog_a6fb6c ...

  8. Solr7.1--- 高亮查询

    由于测试数据比较少,昨天用Java爬了简书的几百篇文章,唉,又特么两点多睡的.如果你需要这些测试文件GitHub. 如果你看过我前面的文章,直接打开db-data-config.xml文件,添加一个e ...

  9. Educational Codeforces Round 53 (Rated for Div. 2) A Diverse Substring

    传送门 https://www.cnblogs.com/violet-acmer/p/10163375.html 题意: 给出串是多态的定义“长度为 n 的串 s ,当串 s 中所有字母出现的次数严格 ...

  10. YII 框架在windows系统下的安装

    第一步,下载yiii框架 http://www.yiichina.com 第二步安装: 1.首先需要下载应用模板,分为基础模板和高级应用模板,这里我以高级应用模板为例子 : 去这里现在高级应用模板 h ...