初识Python asynic异步编程

什么是异步编程?

同步代码(synchrnous code)咱们都很熟悉,就是运行完一个步骤再运行下一个。要在同步代码里面实现"同时"运行多个任务,最简单也是最直观地方式就是运行多个 threads 或者多个 processes。这个层次的『同时运行』多个任务,是操做系统协助完成的。 也就是操做系统的任务调度系统来决定何时运行这个任务,何时切换任务,你本身,做为一个应用层的程序员,是没办法进行干预的。程序员

我相信你也已经据说了什么关于 thread 和 process 的抱怨:process 过重,thread 又要牵涉到不少头条的锁问题。尤为是对于一个 Python 开发者来讲,因为GIL(全局解释器锁)的存在,多线程没法真正使用多核,若是你用多线程来运行计算型任务,速度会更慢。编程

异步编程与之不一样的是,值使用一个进程,不使用 threads,可是也能实现"同时"运行多个任务(这里的任务其实就是函数)。flask

这些函数有一个很是 nice 的 feature:必要的能够暂停,把运行的权利交给其余函数。等到时机恰当,又能够恢复以前的状态继续运行。这听上去是否是有点像进程呢?能够暂停,能够恢复运行。只不过进程的调度是操做系统完成的,这些函数的调度是进程本身(或者说程序员你本身)完成的。这也就意味着这将省去了不少计算机的资源,由于进程的调度必然须要大量 syscall,而 syscall 是很昂贵的。服务器

 

一 定义一个简单的协程:

import asyncio
 
async def execute(x):
    print('Number:', x)
    return x
 
coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')
 
loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)
print('Task:', task)
loop.run_until_complete(task)
print('Task:', task)
print('After calling loop')

# print('Task Result:', task.result())  这样也能查看task执行的结果

运行结果:网络

Coroutine: <coroutine object execute at 0x10e0f7830>
After calling execute
Task: <Task pending coro=<execute() running at demo.py:4>>
Number: 1
Task: <Task finished coro=<execute() done, defined at demo.py:4> result=1>
After calling loop

 

咱们使用 async 定义了一个 execute() 方法,方法接收一个数字参数,方法执行以后会打印这个数字。
随后咱们直接调用了这个方法,然而这个方法并无执行,而是返回了一个 coroutine 协程对象。session

随后咱们使用 get_event_loop() 方法建立了一个事件循环 loop,并调用了 loop 对象的 run_until_complete() 方法将协程注册到事件循环 loop 中,而后启动。最后咱们才看到了 execute() 方法打印了输出结果。
可见,async 定义的方法就会变成一个没法直接执行的 coroutine 对象,必须将其注册到事件循环中才能够执行。

多线程

咱们也能够不使用task来运行,它里面相比 coroutine 对象多了运行状态,好比 running、finished 等,咱们能够用这些状态来获取协程对象的执行状况。app

将 coroutine 对象传递给 run_until_complete() 方法的时候,实际上它进行了一个操做就是将 coroutine 封装成了 task 对象,如:异步

import asyncio

async def execute(x):
    print('Number:', x)

coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')

loop = asyncio.get_event_loop()
loop.run_until_complete(coroutine)
print('After calling loop')
View Code

 查看了源码,正好能够验证上面这一观点:async

run_until_complete()这个方法位于源码中的base_events.py,函数有句注释:
Run until the Future is done.If the argument is a coroutine, it is wrapped in a Task.

 

 

二 发送网络请求结合aiohttp实现异步:

咱们用一个网络请求做为示例,这就是一个耗时等待的操做,由于咱们请求网页以后须要等待页面响应并返回结果。耗时等待的操做通常都是 IO 操做,好比文件读取、网络请求等等。协程对于处理这种操做是有很大优点的,当遇到须要等待的状况的时候,程序能够暂时挂起,转而去执行其余的操做,从而避免一直等待一个程序而耗费过多的时间,充分利用资源。为了测试,我本身先经过flask 建立一个实验环境:

from flask import Flask
import time
 
app = Flask(__name__)
 
@app.route('/')
def index():
    time.sleep(3)
    return 'Hello!'
 
if __name__ == '__main__':
    app.run(threaded=True)

开始测试...

import asyncio
import aiohttp
import time
 
start = time.time()
 
async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    session.close()
    return result
 
async def request():
    url = 'http://127.0.0.1:5000'          # 访问flask搭建的服务器(睡眠3秒),模仿IO阻塞
    print('Waiting for', url)
    result = await get(url)
    print('Get response from', url, 'Result:', result)
 
tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
 
end = time.time()
print('Cost time:', end - start)

运行结果:

Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Cost time: 3.0199508666992188

咱们发现此次请求的耗时由 15 秒变成了 3 秒,耗时直接变成了原来的 1/5。

代码里面咱们使用了 await,后面跟了 get() 方法,在执行这五个协程的时候,若是遇到了 await,那么就会将当前协程挂起,转而去执行其余的协程,直到其余的协程也挂起或执行完毕,再进行下一个协程的执行。

 

二 总结

协程"同时"运行多个任务的基础是函数能够暂停(await实际就是用到了yield)。上面的代码中使用到了 asyncio的 event_loop,它作的事情,本质上来讲就是当函数暂停时,切换到下一个任务,当时机恰当(这个例子中是请求完成了)恢复函数让他继续运行(这有点像操做系统了)。

相关文章
相关标签/搜索