异步编程 101: 是什么、小试Python asyncio

什么是异步编程?

注:本文说的同时是一个直观上感觉的概念,只是为了简化,不是严格意义上的同一时刻。

同步代码(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(协程)。

正常的姿势是调用await visit_async(),就想代码中await asyncio.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?

我的公众号:全栈不存在的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值