什么是异步编程?注:本文说的同时是一个直观上感觉的概念,只是为了简化,不是严格意义上的同一时刻。
同步代码(synchrnous code)我们都很熟悉,就是运行完一个步骤再运行下一个。要在同步代码里面实现"同时"运行多个任务,最简单也是最直观地方式就是运行多个 threads 或者多个 processes。这个层次的『同时运行』多个任务,是操作系统协助完成的。 也就是操作系统的任务调度系统来决定什么时候运行这个任务,什么时候切换任务,你自己,作为一个应用层的程序员,是没办法进行干预的。
我相信你也已经听说了什么关于 thread 和 process 的抱怨:process 太重,thread 又要牵涉到很多头条的锁问题。尤其是对于一个 Python 开发者来说,由于GIL(全局解释器锁)的存在,多线程无法真正使用多核,如果你用多线程来运行计算型任务,速度会更慢。
异步编程与之不同的是,值使用一个进程,不使用 threads,但是也能实现"同时"运行多个任务(这里的任务其实就是函数)。
这些函数有一个非常 nice 的 feature:必要的可以暂停,把运行的权利交给其他函数。等到时机恰当,又可以恢复之前的状态继续运行。这听上去是不是有点像进程呢?可以暂停,可以恢复运行。只不过进程的调度是操作系统完成的,这些函数的调度是进程自己(或者说程序员你自己)完成的。这也就意味着这将省去了很多计算机的资源,因为进程的调度必然需要大量 syscall,而 syscall 是很昂贵的。
异步编程注意事项
有一点是需要格外注意的,异步代码里面不应该用会 block 的函数!也就是说你的代码里面不应该出现下面这些:time.sleep()
会阻塞的 socket
requests.get()
会阻塞的数据库调用
为什么呢?在用 thread 或 process 的时候,代码阻塞了有操作系统来帮你调度,所以才不会出现『一处阻塞,处处傻等』的情况。
但是现在,对于操作系统来说,你的进程就是一个普通的进程,他并不知道你分了哪些不同的任务,一切都要靠你自己了。如果你的代码里出现了阻塞的调用,那么其他部分确实就是傻傻地等着。(等下判断一下这会不会出错)。
小试Python asyncio
Python 版本支持情况asyncio 模块在 Python3.4 时发布。
async 和 await 关键字最早在 Python3.5中引入。
Python3.3之前不支持。
开始动手敲代码
同步版本
就是一个简单的访问百度首页100次,然后打印状态码。
import time
import requests
def visit_sync():
start = time.time()
for _ in range(100):
r = requests.get(URL)
print(r.status_code)
end = time.time()
print("visit_sync tasks %.2f seconds" % (end - start))
if __name__ == '__main__':
visit_sync()
运行一下,发现使用了6.64秒。
异步版本
import time
import asyncio
import aiohttp
async def fetch_async(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
status_code = resp.status
print(status_code)
async def visit_async():
start = time.time()
tasks = []
for _ in range(100):
tasks.append(fetch_async(URL))
await asyncio.gather(*tasks)
end = time.time()
print("visit_async tasks %.2f seconds" % (end - start))
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(visit_async())
有几点说明一下:网络访问的部分变了,前面用的是 requests.get(),这里用的是 aiohttp(不是标准库需要自己安装)。
调用函数的方式变了,前面通过 visit_sync()就可以直接运行,异步代码中不能直接 visit_async(),这会提示你一个 warning:
如果打印一下 visit_async()返回值的类型可以看到,这是一个coroutine(协程)。
正常的姿势是调用 awaitvisit_async(),就想代码中 awaitasyncio.gather(*tasks)一样。但是比较麻烦的一点是 await只有在以关键字 async定义的函数里面使用,而我们的 if__name__=="__main__"里面没有函数,所以可以把这个 coroutine传给一个 eventloop。
loop = asyncio.get_event_loop()
loop.run_until_complete(visit_async())
运行之后发现,耗时0.34秒,效率提升20多倍。(关于如何有逼格地分析异步效率,可以参考前面写过的一篇文章。)
总结一下
事实上,这篇文章已经引出了异步编程中一个重要的概念:协程。『异步编程101』系列文章后面还会花很多篇幅说一说一下协程。
协程"同时"运行多个任务的基础是函数可以暂停(后面我们会讲到这一点是如何实现的,Python 中是通过 yield)。上面的代码中使用到了 asyncio的 event_loop,它做的事情,本质上来说就是当函数暂停时,切换到下一个任务,当时机恰当(这个例子中是请求完成了)恢复函数让他继续运行(这有点像操作系统了)。
这相比使用多线程或多进程,把调度地任务交给操作系统,在性能上有极大的优势,因为不需要大量的 syscall。同时又解决了多线程数据共享带来的锁的问题。而且作为一个应用程序开发者,你应该是要比操作系统更懂,哪些时候进行任务切换。
我个人觉得,新时代的程序员,有两点技能是非常重要的:异步编程的能力和利用多核系统的能力。
觉得不错点个 star?
我的公众号:全栈不存在的