asyncio 协程 异步执行

背景

在抓取Compare, Download & Develop Open Source & Business Software - SourceForge网站内的linux开源软件时,同一款软件有多个版本和对应的多个不同格式的包,按照以往的线性模式,爬取一页29款软件需花费33分钟。共计999页,预计花费22天。

分析得出,大部分时间都在遍历软件的不同文件夹,

我是否可以优化为,同时发送多个请求进行文件夹的遍历,从而解决占用时间过多的问题。

如使用协程进行异步执行。将代码进行异步后发现,爬取信息的速度加快,但网站拥有反扒等措施,同时大量多次访问会显示‘‘访问速度限制’’,无法正常访问,虽然异步后因为网站反扒限制没能解决爬取时间过长的问题,但是异步的方法还是很值得记录一下

接下来仅介绍协程如何使用

事件循环+同步调用API

import asyncio

import time

async def download(url):

    print("start download url")

    time.sleep(2)

    print("end download url")

if __name__ == "__main__":

    start = time.time()

    loop = asyncio.get_event_loop()

    loop.run_until_complete(download("www.baidu.com"))

    print(time.time()-start)

结果:

start download url
end download url
2.0046780109405518

注意:首先asyncio已经帮我们实现了基于各个系统(linux/windows/mac)事件循环模块,比如我们这里通过asyncio.get_event_loop() 获取事件循环对象,有了循环对象我们才能调度协称,获取协称的结果。

我们看到download
下载函数先执行第一个print,再sleep 2s,然后再print,从结果来看就是这样的,没有问题。

但是当我们在并发的情况下,在看看会有什么问题:

#只需要修改loop.run_until_complete这个入参就行

#其他都没变化

loop.run_until_complete(asyncio.wait([download("www.baidu.com"for i in range(2)]))

start download url
end download url
start download url
end download url
4.008697748184204

看到了吧,结果令人非常不适,你期望的结果是2s左右返回,但是为啥4s左右才返回呢?结局真的让人很头大。

罪魁祸首是谁呢?就是因为异步代码中有同步调用,即download协称中有time.sleep同步调用。

事件循环是单线程运行的,所以当sleep之后阻塞单线程,没办法继续运行,所以只能变成同步调用,所以你会看到2个download任务同步返回,耗时4s。

那么如何解决这个问题呢?

事件循环+异步调用API

用asyncio.sleep代替time.sleep

# 把download中time.sleep替换成await asyncio.sleep

await asyncio.sleep(2)

还是上面例子运行如下:

start download url
start download url
end download url
end download url
2.0069119930267334

我们可以看到两个任务并发运行,结果也是我们想要的2s,几乎可以认为两个任务是同时开始同时结束的,这就达到了异步编程的要求:要异步,所有的都要异步

创建任务(Task)或者未来对象(Future)

上面我们讲解了事件循环如何调用协称来实现异步编程,其实事件循环不但可以调用协称,而且还可以调度Task或者Future对象,比如:

1

2

只需要修改loop.run_until_complete的入参即可 其他不用修改

loop.run_until_complete(asyncio.ensure_future(download("www.baidu.com")))

这里是通过asyncio.ensure_future创建Future对象,

run_until_complete的入参是多选的,

协称/Future/Task都可以当作参数传递进去,比如创建Task对象:

# 解释同上

loop.run_until_complete(loop.create_task(download("www.baidu.com")))

通过loop.create_task创建Task对象,run_until_complete照样可以接受,为什么呢?

因为Task是Future的派生类,所以二者都可以当作入参传递。

还有asyncio.ensure_future的底层就是调用loop.create_task,所以看似ensure_future是和事件循环无关,

但其实内部是有获取事件循环并且调用create_task的。

部分源码:

def ensure_future(coro_or_future, *, loop=None):

    if coroutines.iscoroutine(coro_or_future):

        if loop is None//没有事件循环

            loop = events.get_event_loop() //获取loop, 这个和main中外面获取的loop是同一个,因为在单线程中并且是线程安全的。

        task = loop.create_task(coro_or_future)

以上两者运行结果都是一样的,如下:

start download url
end download url
2.0043089389801025

asyncio.wait和asyncio.gather的高阶API用法

asyncio.wait我们上面示例都在用,大致意思就是运行所有协称/Task/Future,直到他们完成,而它本身就是一个协称,所以可以被事件循环run_until_complete调度。

我们重点说下asyncio.gather的用法,这个函数是high-level的,就是高度抽象的,它比wait更加灵活和方便。比如:

# 和wait用法一样

loop.run_until_complete(asyncio.gather(download("www.baidu.com")))

如果更加灵活呢?

# 示例1 和wait差不多,就是前面得加*

    tasks = [download("www.baidu.com"for i in range(2)]

    loop.run_until_complete(asyncio.gather(*tasks))

# 示例2:分组运行

    tasks1 = [download("www.baidu.com"for i in range(2)]

    tasks2 = [download("www.baidu.com"for i in range(2)]

    loop.run_until_complete(asyncio.gather(*tasks1, *tasks2))

如果想要接受返回值呢?

import asyncio

import time

# download上面定义过了

async def run():

    tasks1 = [download("www.baidu.com"for i in range(2)]

    tasks2 = [download("www.baidu.com"for i in range(2)]

    ret = await asyncio.gather(*tasks1, *tasks2)

    print(ret)

if __name__ == "__main__":

    start = time.time()

    asyncio.run(run())

    print(time.time()-start)

start download url

start download url

start download url

start download url

end download url

end download url

end download url

end download url

['hello world''hello world''hello world''hello world']

2.005855083465576

我们看到各个协称执行的结果也都获取到了,日常开发中建议多用gather

协称/Task/Future执行完之后想要回调怎么办?

很简单,虽然这种情况我们不多见,但是万一在项目中需要回调,那么只需要几行代码就可以搞定回调。

import asyncio

import time

from functools import partial

async def download(url):

    print("start download url")

    await asyncio.sleep(2)

    print("end download url")

    return "hello world" //协称返回的结果

def callback(url, future): //协称在return之前的回调

    print("回调了:", url)

if __name__ == "__main__":

    start = time.time()

    loop = asyncio.get_event_loop()

    future = asyncio.gather(download("www.baidu.com"))

    future.add_done_callback(partial(callback, "www.baidu.com")) //利用partial函数包装callback,因为add_done_callback添加回调只接受一个参数,所以这里必须得用partial包装成一个函数,那相应的callback需要在增加一个参数url,而且这个url必须放在参数前面,这样的话我们就可以在回调的时候传递多个参数了。

    loop.run_until_complete(future)

    print("协称运行的结果:", future.result())

    print(time.time()-start)

运行结果:

start download url
end download url
回调了: www.baidu.com
协称运行的结果: ['hello world']
2.0060088634490967

syncio的高阶API和事件循环相结合起来,可以在python的异步编程中发挥巨大作用,你只需要像编写同步代码那样编写异步代码,只是异步多了几个语法规则而已,只有多做才能熟练。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值