asyncio是解决异步io高并发编程的核心模块,python3.4后开始引用,可以说是python中最具野心的一个模块,无论是高并发web服务器还是高并发爬虫都可以胜任。
asyncio提供了异步IO编程的一整套方案,包括:
futures
模块但适用于事件循环使用的Future类。yield from
的协议和任务,可以使用同步编码的方式编写异步并发编码
threading
模块中的同步原语,可以用在单线程内的协程之间。asyncio异步IO高并发编程依然离不开三要素,即事件循环
、回调模式(协程模式中又叫做驱动生成器/协程)
和IO多路复用(epoll)
。有很多优秀的框架都是基于asyncio模块的,比如说Tornado、gevent、twisted(scrapy、django channels)
事件循环
是每个 asyncio 应用的核心。 事件循环会运行异步任务和回调,执行网络 IO 操作,以及运行子进程。一个线程只能有一个事件循环。它实现了管理事件的所有功能。asyncio提供用于管理事件循环的方法如下:
asyncio.get_running_loop()
:返回当前 OS 线程中正在运行的事件循环。如果没有正在运行的事件循环则会引发 RuntimeError。 此函数只能由协程或回调来调用。asyncio.get_event_loop()
:获取当前事件循环。如果当前OS线程为主线程,没有设置事件循环,并且没有调用set_event_loop(),asyncio将自动创建一个新的事件循环并将其设置为当前事件循环。asyncio.set_event_loop(loop)
:将 loop 设置为当前 OS 线程的当前事件循环。asyncio.new_event_loop()
:创建一个新的事件循环。# 使用asyncio
import asyncio
import time
async def get_html(url):
print("start get url")
await asyncio.sleep(2) # 返回一个future对象
print("end get url")
if __name__ == '__main__':
start_time = time.time()
loop = asyncio.get_event_loop()
tasks = [get_html("123") for i in range(10)]
loop.run_until_complete(asyncio.wait(tasks))
print(time.time() - start_time)
# 获取协程的返回值
import asyncio
import time
# 返回函数
from functools import partial
async def get_html(url):
print("start get url")
await asyncio.sleep(2)
return "zzh"
def callback(url, future):
print("send email to zzh {}".format(url))
if __name__ == '__main__':
start_time = time.time()
loop = asyncio.get_event_loop()
# 使用ensure_future获取协程返回值
get_future = asyncio.ensure_future(get_html("123"))
"""
源码:
if coroutines.iscoroutine(coro_or_future):
if loop is None:
loop = events.get_event_loop()
task = loop.create_task(coro_or_future)
一个线程只有一个loop。asyncio.ensure_future没有传入loop源码中会获取当前loop。
然后在使用create_task获取返回值。
"""
# loop.run_until_complete(get_future)
# print(get_future.result())
# 使用create_task获取值
task = loop.create_task(get_html("123"))
task.add_done_callback(partial(callback, "456"))
loop.run_until_complete(task)
print(task.result())
# print(time.time() - start_time)
loop.run_forever()
:一直运行事件循环直到 stop() 被调用。loop.stop():
停止事件循环。loop对象会传入到Task/Future对象中,所以通过任意一个Task/Future都可以停止loop。loop.run_until_complete(future)
:run_forever()
,在future执行完毕后调用stop()
asyncio.task
来运行。Future
的结果或者引发相关异常。# loop会被放到future中
import asyncio
loop = asyncio.get_event_loop()
loop.run_forever()
# 源码 asyncio/base_events.py
# 运行完会执行_run_until_complete_cb方法
future.add_done_callback(_run_until_complete_cb)
def _run_until_complete_cb(fut):
if not fut.cancelled():
exc = fut.exception()
if isinstance(exc, BaseException) and not isinstance(exc, Exception):
# Issue #22429: run_forever() already finished, no need to
# stop it.
return
futures._get_loop(fut).stop()
loop.call_soon(callback, *args, context=None):
loop.call_soon_threadsafe(callback, *args, context=None):
loop.call_later(delay, callback, *args, context=None):
loop.call_at(when, callback, *args, context=None):
import asyncio
def callback(sleep_time, loop):
print("success time {}".format(sleep_time))
def stop_loop(loop):
loop.stop()
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop_time = loop.time()
loop.call_at(loop_time+2, callback, 2, loop)
loop.call_at(loop_time+1, callback, 1, loop)
loop.call_at(loop_time+3, callback, 3, loop)
loop.call_soon(callback, 4, loop)
# loop.call_soon(callback, 2, loop)
# loop.call_soon(stop_loop, loop)
loop.run_forever()
"""
success time 4
success time 2
success time 1
success time 2
success time 3
"""
loop.create_future():
创建一个附加到事件循环中的 asyncio.Future对象。loop.create_task(coro, *, name=None):
安排一个协程的执行。返回一个 Task 对象。awaitable loop.run_in_executor(executor, func, *args):
协程是单线程任务调度方案,一般不要在协程中加入阻塞代码,如果一定需要阻塞代码,可以和多线程和多进程结合起来完成整套解决方案,把费时的阻塞IO操作通过协程调度到线程池或进程池中。
#使用多线程:在协程中集成阻塞io
import asyncio
from concurrent.futures import ThreadPoolExecutor
import socket
from urllib.parse import urlparse
def get_url(url):
#通过socket请求html
url = urlparse(url)
host = url.netloc
path = url.path
if path == "":
path = "/"
#建立socket连接
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# client.setblocking(False)
client.connect((host, 80)) #阻塞不会消耗cpu
#不停的询问连接是否建立好, 需要while循环不停的去检查状态
#做计算任务或者再次发起其他的连接请求
client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
data = b""
while True:
d = client.recv(1024)
if d:
data += d
else:
break
data = data.decode("utf8")
html_data = data.split("\r\n\r\n")[1]
print(html_data)
client.close()
if __name__ == "__main__":
import time
start_time = time.time()
loop = asyncio.get_event_loop()
executor = ThreadPoolExecutor(3)
tasks = []
for url in range(20):
url = "http://shop.projectsedu.com/goods/{}/".format(url)
task = loop.run_in_executor(executor, get_url, url)
tasks.append(task)
loop.run_until_complete(asyncio.wait(tasks))
print("last time:{}".format(time.time()-start_time))
import asyncio
from urllib.parse import urlparse
async def get_url(url):
url = urlparse(url)
host = url.netloc
path = url.path
if path == "":
path = "/"
reader, writer = await asyncio.open_connection(host, 80)
writer.write("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
all_lines = []
async for raw_line in reader:
data = raw_line.decode("utf8")
all_lines.append(data)
html = "\n".join(all_lines)
return html
async def main():
tasks = []
for url in range(20):
url = "http://shop.projectsedu.com/goods/{}/".format(url)
tasks.append(asyncio.ensure_future(get_url(url)))
for task in asyncio.as_completed(tasks):
result = await task
print(result)
if __name__ == "__main__":
import time
start_time = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
print('last time:{}'.format(time.time()-start_time))
asyncio.isfuture(obj)
如果 obj 为下面任意对象,返回 True:
asyncio.Future
类的实例.asyncio.Task
类的实例.asyncio.ensure_future(obj, *, loop=None)
返回:import asyncio
async def get_html(url):
print("start get url")
await asyncio.sleep(2)
return "success"
if __name__ == '__main__':
loop = asyncio.get_event_loop()
get_future = asyncio.ensure_future(get_html("liuchongyu.com"))
task = loop.create_task(get_html("liuchongyu.com"))
loop.run_until_complete(task)
print(get_future.result())
# print(task.result())
使用 asyncio.ensure_future()
和loop.create_task()
都可以创建一个task
,我们查看asyncio.ensure_future()的源码中可以发现,如果没有传入loop,会自动获取事件循环一个线程只能有一个事件循环,所以和在外部手动获取事件循环是一样的,然后通过loop.create_task()
创建一个task。
# ###源码 asyncio/tasks.py
def ensure_future(coro_or_future, *, loop=None):
if coroutines.iscoroutine(coro_or_future):
if loop is None:
loop = events.get_event_loop()
task = loop.create_task(coro_or_future)
if task._source_traceback:
del task._source_traceback[-1]
return task
elif futures.isfuture(coro_or_future):
if loop is not None and loop is not futures._get_loop(coro_or_future):
raise ValueError('loop argument must agree with Future')
return coro_or_future
elif inspect.isawaitable(coro_or_future):
return ensure_future(_wrap_awaitable(coro_or_future), loop=loop)
else:
raise TypeError('An asyncio.Future, a coroutine or an awaitable is '
'required')
所以,一般使用create_task()函数,它是创建新task的首选途径。
class asyncio.Future(*, loop=None):
一个 Future
代表一个异步运算的最终结果。非线程安全。Future
是一个 awaitable
对象。协程可以等待 Future 对象直到它们有结果或异常集合或被取消。
result():
返回 Future 的结果。set_result(result):
将 Future 标记为 完成 并设置结果。set_exception:
将 Future 标记为 完成 并设置一个异常。done():
如果 Future 为已 完成 则返回 True 。cancelled():
如果 Future 已 取消 则返回 Trueadd_done_callback(callback, *, context=None):
添加一个在 Future 完成 时运行的回调函数。remove_done_callback(callback):
从回调列表中移除 callback 。cancel(msg=None):
取消 Future 并调度回调函数。exception():
返回 Future 已设置的异常。get_loop():
返回 Future 对象已绑定的事件循环。await
语句中只能使用可等待对象awaitable
。可等待对象有三种主要类型: 协程
, Task
和Future
.
asyncio.run(coro, *, debug=False)
执行 coroutine coro
并返回结果。 此函数会运行传入的协程,负责管理 asyncio 事件循环,终结异步生成器,并关闭线程池。 当有其他 asyncio 事件循环在同一线程中运行时,此函数不能被调用。 如果 debug 为 True,事件循环将以调试模式运行。 此函数总是会创建一个新的事件循环并在结束时关闭之。它应当被用作 asyncio程序的主入口点,理想情况下应当只被调用一次。
asyncio.create_task(coro, *, name=None)
将 coro 协程
打包为一个 Task
排入协程准备执行。返回 Task 对象。 name 不为 None,它将使用Task.set_name()
来设为任务的名称。 该任务会在 get_running_loop()返回的循环中执行,如果当前线程没有在运行的事件循环则会引发 RuntimeError
。
coroutine asyncio.sleep(delay, result=None, *, loop=None)
asyncio.gather(*aws, loop=None, return_exceptions=False)
并发运行aws序列中的可等待对象。
return_exceptions
为 False (默认),所引发的首个异常会立即传给等待 gather() 的任务。aws 序列中的其他可等待对象不会被取消并将继续运行。return_exceptions
为 True,异常会和成功的结果一块处理,并聚合至结果列表。和wait()方法类似,区别是gather更高级,传入的是awaitable的序列,还可以将分组后的序列传入,并且可以取消序列中的某个Task/gather。
awaitable asyncio.shield(aw, *, loop=None)
coroutine asyncio.wait_for(aw, timeout, *, loop=None):
timeout
可以为 None,也可以为 float 或 int 型数值表示的等待秒数。如果 timeout 为 None,则等待直到完成。asyncio.TimeoutError.
shield()
。asyncio.wait(aws, *, loop=None, timeout=None, return_when=ALL_COMPLETED)
done, pending = await asyncio.wait(aws)
class asyncio.Task(coro, *, loop=None, name=None)
Task是futures.Future
的子类,被用来在事件循环中运行协程,是Future和协程之间的一个桥梁,封装了一些之前操作协程来生成Future的一些方法。
比如说,我们在定义一个协程后,在驱动这个协程前需要自己使用next()
或者send(None)
预激协程,Task中则将send(None)
方法封装起来自动调用。
再比如说,在Future中,需要捕捉StopIteration
异常,并将异常值用set_result()
方法放到Future中,在线程池中是submit
方法实现这个功能的,而Task则将这个过程封装进来自动调用。
# 源码
def __step(self, exc=None):
# ...
try:
if exc is None:
result = coro.send(None)
else:
result = coro.throw(exc)
except StopIteration as exc:
if self._must_cancel:
self._must_cancel = False
super().set_exception(futures.CancelledError())
else:
super().set_result(exc.value)
Pending
:创建future,还未执行Running
:事件循环正在调用执行任务Done
:任务执行完毕Cancelled
:Task被取消后的状态asyncio.Task
从Future 继承了其除Future.set_result()
和Future.set_exception()
以外的所有 API。
cancel(msg=None)
取消一个正在运行的 Task 对象可使用 cancel()
方法。 下一轮事件循环中抛出一个 CancelledError
异常给被封包的协程。。如果取消期间一个协程正在等待一个 Future
对象,该Future
对象也将被取消。
async def delay(sleep_times):
print("waiting")
await asyncio.sleep(sleep_times)
print("done after {}s".format(sleep_times))
if __name__ == '__main__':
task1 = delay(1)
task2 = delay(2)
task3 = delay(3)
tasks = [task1, task2, task3]
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(asyncio.wait(tasks))
except KeyboardInterrupt as e:
all_task = asyncio.Task.all_tasks()
for task in all_task:
print("cancel task")
print(task.cancel())
# run_until_complete()方法在所有task执行完成后会自动调用stop,
# 但是如果使用cancel取消掉task,就不能自动执行stop()方法,所以需要手动运行
loop.stop()
# 如果线程中没有时间循环处于panding状态会报错
loop.run_forever()
finally:
loop.close()
add_done_callback(callback, *, context=None)
添加一个回调,将在 Task 对象 完成 时被运行。此方法应该仅在低层级的基于回调的代码中使用。callback
传入的是函数名,如果想要传入参数,可以使用偏函数partial
将回调函数和参数封装。Steam
是用于处理网络连接的高级 async/await-ready
原语。Steam允许发送和接收数据,而不需要使用回调或低级协议和传输。
asyncio.open_connection(host=None, port=None, *, loop=None, limit=None, ssl=None, family=0, proto=0, flags=0, sock=None, local_addr=None, server_hostname=None, ssl_handshake_timeout=None)
asyncio.open_connection()
是一个协程,用来建立网络连接并返回一对 (reader, writer) 对象。reader和writer
对象是 StreamReader
和 StreamWriter
类的实例。loop
参数是可选的,当从协程中等待该函数时,总是可以自动确定。limit
确定返回的 StreamReader 实例使用的缓冲区大小限制。默认情况下,limit 设置为 64 KiB 。loop.create_connection()
。reader, writer = await asyncio.open_connection(host, 80)
使用asyncio.open_connection()
我们依然需要像使用回调模式中那样,建立socket连接 > 设置为非阻塞IO > register到epoll/select中监听其IO状态
,这在asyncio.open_connection()都实现了。
coroutine asyncio.start_server(client_connected_cb, host=None, port=None, *, loop=None, limit=None, family=socket.AF_UNSPEC, flags=socket.AI_PASSIVE, sock=None, backlog=100, ssl=None, reuse_address=None, reuse_port=None, ssl_handshake_timeout=None, start_serving=True)
client_connected_cb
会被调用。该函数会接收到一对参数(reader, writer),reader
是类 StreamReader
的实例,而writer
是类 StreamWriter
的实例。client_connected_cb
即可以是普通的可调用对象也可以是一个 协程函数; 如果它是一个协程函数,它将自动作为 Task 被调度。await
该方法时,该参数始终可以自动确定。loop.create_server()
.asyncio.StreamReader
这个类表示一个读取器对象,该对象提供api以便于从IO流中读取数据。不推荐直接实例化 StreamReader 对象,建议使用 open_connection()
和 start_server()
来获取 StreamReader 实例 reader 。
reader.read(n=-1)
readline()
readexactly(n)
readuntil(separator=b'\n')
separator
LimitOverrunError
异常,数据将留在内部缓冲区中并可以再次读取。IncompleteReadError
异常,并重置内部缓冲区。 IncompleteReadError.partial
属性可能包含指定separator
的一部分。asyncio.StreamWriter
这个类表示一个写入器对象,该对象提供api以便于写数据至IO流中。不推荐直接实例化 StreamReader 对象,建议使用 open_connection()
和 start_server()
来获取 StreamReader
实例 reader
。
write(data)
write(data)
方法中封装了send()
方法,即可以直接传入http请求头drain()
方法一起使用:writer.write("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
stream.write(data)
await stream.drain()
writelines(data)
drain()
方法一起使用:stream.writelines(lines)
await stream.drain()
close()
wait_closed()
方法一起使用:stream.close()
await stream.wait_closed()
can_write_eof()
:如果基础传输支持write_eof()方法,则返回True,否则返回False。write_eof()
:刷新缓冲的写入数据后关闭writer。transport
:返回基础异步传输。get_extra_info(name, default=None)
:访问可选的传输信息;coroutine drain()
:等待,直到适合继续写入流为止。writer.write(data)
await writer.drain()
这是一种与基础IO写缓冲区交互的流控制方法。 当缓冲区的大小达到高水平线时,drain()会阻塞,直到缓冲区的大小排空到低水平线为止,然后才能恢复写入。 当没有什么可等待的时,drain()立即返回。
is_closing()
:如果steam已关闭或正在关闭,则返回True。coroutine wait_closed()
:等待,直到steam关闭。应该在close()之后调用,以等待基础连接关闭。import asyncio
async def computer(x, y):
print("Computer %s + %s ..." % (x, y))
await asyncio.sleep(1)
return x + y
async def print_sum(x, y):
result = await computer(x, y)
print("%s + %s = %s" % (x, y, result))
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(print_sum(1, 2))
loop.close()
task可以看作是调用方,print_num是委托生成器,computer是子生成器,Task 对象被用来在事件循环中运行调度协程。
使用IO多路复用+回调模式+事件循环
完成http请求,为了解决
为了解决回调之痛,引出了协程,我们现在尝试用基于协程的asyncio,即IO多路复用+协程+事件循环
的模式来模拟http请求。
原生asyncio目前没有提供http协议的接口,提供的是更底层的tcp/udp,基于aiohttp框架是专门用来作http请求的。
import asyncio
import time
async def get_url(url):
# 解析url
url = urlparse(url)
host = url.netloc
path = url.path
if path == "":
path = "/"
# 建立socket连接
reader, writer = await asyncio.open_connection(host, 80)
writer.write("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
all_lines = []
async for raw_line in reader:
data = raw_line.decode("utf-8")
all_lines.append(data)
html = "\n".join(all_lines)
# print(html)
return html
async def main():
# 将所有任务打包成Future/Task对象放入tasks中
tasks = []
for url in range(20):
url = "http://shop.projectsedu.com/goods/{}/".format(url)
tasks.append(asyncio.ensure_future(get_url(url)))
# 调用as_completed()返回一个生成器,首先找出调用此方法时就已经执行完成或者取消的Future/Task
for task in asyncio.as_completed(tasks):
result = await task
print(result)
if __name__ == '__main__':
start_time = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
print("last time:{}".format(time.time()-start_time))
asyncio
是基于单线程的协程,是不涉及到GIL锁的问题,在协程中,除await语句外,其他语句都是在CPU中计算,在当前协程一旦开始执行就会顺序执行完成后才会切换到另一个协程中,因此一般不需要担心同步问题。
但是有时候会遇到两个协程都要await到同一个子协程中,向这个子协程请求一个结果,如果子协程是一个高耗时的IO操作,就会导致这两个协程都要各自请求一次这个高耗时的IO操作,这就涉及到协程间的同步问题。
如下面这个例子,协程get_stuff()是请求一个url获得返回值的高耗时IO操作。当parse_stuff()协程向get_stuff()发出请求解析某个url的请求后,get_stuff()开始执行这个url的下载请求,在stuff = await aiohttp.request(‘GET’, url)处暂停,继续其他协程的执行,这时parse_stuff()也请求了同一个url的获取请求,因为上一次对这个url的请求还没有返回,所以cache中还没有返回值,所以会再次发起一次stuff = await aiohttp.request(‘GET’, url) 请求。这样会耗费大量时间。如果在get_stuff(url)中加锁,一个协程请求后,只能在执行完释放锁后,另一个协程才可以再次请求。
import asyncio
import aiohttp
from asyncio import Lock, Queue
cache = {}
lock = Lock()
async def get_stuff(url):
# with await lock:
# await lock.acquire()
async with lock:
if url in cache:
return cache[url]
stuff = await aiohttp.request('GET', url)
cache[url] = stuff
return stuff
async def parse_stuff():
stuff = await get_stuff()
# do some parsing
async def use_stuff():
stuff = await get_stuff()
# use stuff to do something interesting
if __name__ == '__main__':
tasks = [parse_stuff(), use_stuff()]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
asyncio也为我们提供了一系列同步机制,是程序员级别的锁,不深入到操作系统中去。
await lock.acquire()
锁定并执行这个协程,lock.release()
解锁,因为acquire()方法肩负执行协程的任务,必须异步执行,所以是一个协程。# 源码
class Lock(_ContextManagerMixin):
# ...
async def acquire(self):
if not self._locked and all(w.cancelled() for w in self._waiters):
self._locked = True
return True
# 创建Future
fut = self._loop.create_future()
# 把Future加入执行队列中
self._waiters.append(fut)
try:
try:
# 异步等待fut的完成
await fut
finally:
# 完成后将fut从队列中移除
self._waiters.remove(fut)
except futures.CancelledError:
if not self._locked:
self._wake_up_first()
raise
# 将判断是否获取锁的标志_locked置为True
self._locked = True
return True
def release(self):
if self._locked:
# 将判断是否获取锁的标志_locked置为False
self._locked = False
self._wake_up_first()
else:
raise RuntimeError('Lock is not acquired.')
# ...
多线程中的Queue内部使用Condition会发生阻塞,当队列已满put()
就会阻塞,当队列已空,get()
就会阻塞。异步编程中不能存在阻塞
,所以不能使用多线程中的Queue完成通信,需要使用asyncio中提供的Queue
。 asyncio中的Queue中的接口和多线程中的是一样的,但是其中put和get方法实现了协程。 asyncio中的Queue可以控制最大长度,即限流,如果没有限流的需求,可以在单线程
中申请一个全局的List完成通信
# 源码 asyncio/queues.py
class Queue:
async def put(self, item):
"""
将项目放入队列。 如果队列已满,请等到空闲插槽可用后再添加项目。
"""
while self.full():
putter = self._loop.create_future()
self._putters.append(putter)
try:
await putter
except:
putter.cancel() # Just in case putter is not done yet.
try:
# Clean self._putters from canceled putters.
self._putters.remove(putter)
except ValueError:
# The putter could be removed from self._putters by a
# previous get_nowait call.
pass
if not self.full() and not putter.cancelled():
# We were woken up by get_nowait(), but can't take
# the call. Wake up the next in line.
self._wakeup_next(self._putters)
raise
return self.put_nowait(item)
async def get(self):
"""
如果队列为空,请等待直到有一个项目可用。
"""
while self.empty():
getter = self._loop.create_future()
self._getters.append(getter)
try:
await getter
except:
getter.cancel() # Just in case getter is not done yet.
try:
# Clean self._getters from canceled getters.
self._getters.remove(getter)
except ValueError:
# The getter could be removed from self._getters by a
# previous put_nowait call.
pass
if not self.empty() and not getter.cancelled():
# We were woken up by put_nowait(), but can't take
# the call. Wake up the next in line.
self._wakeup_next(self._getters)
raise
return self.get_nowait()