Asyncio 使用经验

Asyncio 使用经验

这是我在知乎上的第一篇文章。主要是介绍一下Python 3.4版本之后的Asyncio库的使用,记录一下自己的使用经验。

什么是Asyncio

这里摘抄一段文档中的说明

This module provides infrastructure for writing single-threaded concurrent code using coroutines, multiplexing I/O access over sockets and other resources, running network clients and servers, and other related primitives.

从名字上我们可以看到这个库就是为了解决异步IO而存在的。

在日常的使用中,我们会遇到的IO主要有:磁盘读写IO以及网络IO,使用asyncio可以帮助我们更加高效的使用资源,提高程序的响应,而不是让程序顺序排队等在IO上。

如何使用Asyncio

在python中使用asyncio还是比较方便的,这里摘抄一段文档中的使用例子

import asyncio

def hello_world(loop):
    print('Hello World')
    loop.stop()

loop = asyncio.get_event_loop()

# Schedule a call to hello_world()
loop.call_soon(hello_world, loop)

# Blocking call interrupted by loop.stop()
loop.run_forever()
loop.close()

这是文档中的例子,这个例子在其他asyncio的文章中也是常见的啦。

当然在日常使用中,我们经常会调用run_until_complete()方法,稍微改造一下上面的例子:

import asyncio

async def hello_world():
    print('Hello World')
    await asyncio.sleep(1)

loop = asyncio.get_event_loop()
loop.run_until_complete(hello_world())
loop.close()

如果你使用的是3.4版本的python,请使用装饰器来描述hello_world这个协程。

从上面的例子,有几个关键点需要体现一下:

1、asyncio.get_event_loop()

2、用async描述的hello_world方法

3、loop.run_until_complete()

抽象的一个比喻:首先使用get_event_loop()方法获取到一条传输带,使用async(或者是装饰器)包装一下方法,丢到传输带上,最后使用run_until_complete()(或者run_forever())开启电源,让这个传输带工作起来。期间我们不需要管工人啥时候处理传送带上的方法,我们只要丢进去就好了。

这样的比喻不知道贴切不贴切,这个是我认为比较好的一种理解asyncio的方式了。

经过上面的例子,我们依旧没有感受到asyncio带来的牛逼,调用一个hello_world还费那么大劲。

那我们来点实际一些的例子(以下场景纯属YY):

小李接到上级任务:需要做一个爬虫

没错,上级的任务就是这样简单粗暴,要做一个爬虫。

小李是一个老搬砖工,一个爬虫而已,轻车熟路啦,reqeusts请求,BeautifulSoup解析,最后保存数据完工。

但是小李这几天看到qq群里有人在吹asyncio,他看完介绍之后觉得,这个东西,牛逼、酷、炫。加上项目不赶,所以他决定使用asyncio来实现这个爬虫。

他分析了一下,决定使用多线程发起网络请求,多进程来解析dom,最后保存依旧使用多线程的方式来实现。

既然是老码农,没啥好说的,抄起键盘就是一顿操作,得出了如下代码:

class XiaoLiSpider(object):
    def __init__(self) -> None:
        self._urls = [
            "http://www.fover.cn/da/"
            "http://www.fover.cn/sha/"
            "http://www.fover.cn/bi/"
        ]
        self._loop = asyncio.get_event_loop()
        self._thread_pool = concurrent.futures.ThreadPoolExecutor(
            max_workers=10)
        self._process_pool = concurrent.futures.ProcessPoolExecutor()

    def crawl(self):
        a = []
        for url in self._urls:
            a.append(self._little_spider(url))
        self._loop.run_until_complete(asyncio.gather(*a))
        self._loop.close()
        self._thread_pool.shutdown()
        self._process_pool.shutdown()

    async def _little_spider(self, url):
        response = await self._loop.run_in_executor(
            self._thread_pool, self._request, url)
        urls = await self._loop.run_in_executor(self._process_pool,
                                                self._biu_soup,
                                                response.text)
        self._save_data(urls)
        print(urls)

    def _request(self, url):
        return requests.get(url=url,timeout=10)

    @classmethod
    def _biu_soup(cls, response_str):
        soup = BeautifulSoup(response_str, 'lxml')
        a_tags = soup.find_all("a")
        a = []
        for a_tag in a_tags:
            a.append(a_tag['href'])
        return a

    def _save_data(self,urls):
        #保存思路如上,这里偷懒了大家根据实际需要写
        pass

没错就是这样简单的代码实现了,多线程请求,多进程解析的操作,非常简单,这让我想起了Android开发中,使用rxAndroid可以方便的切换线程的快感。

扯远了,回到小李的代码,这里有几个点需要注意的

1、asyncio.gather(*a) 这里将任务做个集合

2、run_in_executor()是一个好方法,可以方便的让我们使用线程池和进程池

3、注意在使用进程池执行任务的时候,需要加上 @classmethod装饰,因为多进程不共享内存,当然网上有更加详细的解释,大家可以上网搜一下具体的资料,我这里简答的描述为由于内存共享问题,所以多进程调用方法必须是无副作用的。

4、用完记得关。close()、shutdown() 牢记心间,要不然定时任务跑多了,你会发现一堆进程在那边吃着cpu耗着memory在看戏。

加速Asyncio

小李写完这个爬虫之后,就向同组的大哥老王炫耀,老王,看了眼小李的代码。

说到:吼啊!你这个asyncio用的还可以啊,不过还可以跑得更快。

小李:我当然是支持它跑得更快的。

老王:那我钦点uvloop,这个使用库可以有效的加速asyncio,本库基于libuv,也就是nodejs用的那个库。github地址

MagicStack/uvloopgithub.com图标

使用它也非常方便

import asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

没错就是2行代码,就可以提速asyncio,效果大概是这样的:

小李看完捋了捋自己的头发,想着如果有一天自己的脑袋也像老王这样油亮光滑,自己的编程水平也和他差不多了吧。

Asyncio生态

目前生态上,常见的异步库都有,由于我并没有太多深入使用的经验,所以我引用知乎上另一篇帖子,关于asyncio生态的讨论。

截止到 2018 年 1 月,Python 的 asyncio 的生态如何?www.zhihu.com图标


最后说几句

之前的例子我选用requests,因为很多程序员熟悉reqeusts这个库,这个库是阻塞库,也就是传统方式的类似BIO(Blocking IO)的方式,新的比较流行的网络请求库是aiohttp,可以使用这个库发起类似NIO(Not Blocking IO)的请求,但是相比reqeusts的流行度,大家还得再去熟悉aiohttp这一套,还是需要一些时间的学习成本的,使用run_in_executor() 方法可以在原有的基础上写异步程序,当然肯定没有aiohttp快,以及节省资源啦。

发布于 2018-03-15