美文网首页异步
这怕是全网最详细的异步IO之协程详解!

这怕是全网最详细的异步IO之协程详解!

作者: 小志Codings | 来源:发表于2021-08-19 19:19 被阅读0次

大家好,我是剑南,今天我为大家带来的内容是python异步IO的协程知识的分享。

为何引出协程

协程是python中比较难理解的知识。在python中执行多任务可以采用多进程或者是多线程来执行,但是两者都存在一些缺点,因此,我们提出了协程。

多线程优缺点分析

优缺点分析

优点:在一个进程内线程共享全局变量,多线程方便共享数据。

缺点:线程可以对全局变量随意的修改,这会造成线程之间对全局变量的混乱(即线程非安全)

引入案例

import threading
import time


def test1(num_list):
    num_list.append(10000)
    print('test1--->num:%s' % num_list)


def test2(num_list):
    print('test2--->num:%s' % num_list)



if __name__ == "__main__":
    num_list = [11, 22, 33, 44]
    t1 = threading.Thread(target=test1, args=(num_list, ))
    t1.start()

    time.sleep(1)

    t2 = threading.Thread(target=test2, args=(num_list, ))
    t2.start()

运行结果,如下所示:

test1--->num:[11, 22, 33, 44, 10000]
test2--->num:[11, 22, 33, 44, 10000]

多进程优缺点分析

优缺点分析

优点:稳定性强。

缺点:创建进程的代价非常大,因为操作系统要给每个进程分配固定的资源,并且操作系统对进程的总数会有一定的限制,若线程过多,操作系统的调度都会出现问题,会造成假死状态。

同步与异步的理解

同步:当发出一个“调用”时,在没有得到结果之前,该“调用”就不用返回,“调用者”需要一直等待,直到”调用“结束,才能进行下一步工作。

异步:”调用“发出之后,就直接返回了,即使是没有返回结果也没有问题,当”被调用者“完成任务后,通过状态通知”调用者“继续回来处理该调用。

接下来我们就来了解一下普通同步代码实现的多IO任务的案例。具体代码,如下所示:

import time


def testIO_1():
    print('开始运行IO任务1....')
    time.sleep(2)   # 假设该任务运行2秒
    print('IO任务已经完成,耗时2秒')



def testIO_2():
    print('开始运行IO任务2....')
    time.sleep(3)   # 假设该任务运行3秒
    print('IO任务已经完成,耗时3秒')


start = time.time()
testIO_1()
testIO_2()
print('所有IO任务,总耗时%.5f秒' % float(time.time() - start))

运行结果,如下所示:

开始运行IO任务1....
IO任务已经完成,耗时2秒
开始运行IO任务2....
IO任务已经完成,耗时3秒
所有IO任务,总耗时5.04647秒

在上面的代码中,我们实现了顺序两个同步IO任务testIO_1()与testIO_2(),最后总耗时为5秒。我们都知道,在计算机中CPU的运算速率要远远大于IO速率,而当CPU运算完毕之后,如果再要闲置很长时间去等待IO任务完成才能进行下一个任务的计算,这样执行任务的效率很低。

因此,我们可以想到,能否在上述IO任务执行前中断当前IO任务(对应time.sleep(2)),进入下一个任务,当IO任务完成之后再唤醒该任务

因此,协程便诞生了。

async与await实现协程

async与await这两个关键字是出现在python3.4之后的,代替了python2版本中的yield from。

async:声明一个函数为异步函数,异步函数的特点是能在函数执行过程中挂起,去执行其他异步函数,等待挂起条件(time.sleep(3))消失后,再回来执行。

await:声明程序挂起,比如异步程序执行到某一步时需要很长时间的等待,就将此挂起,去执行其他异步函数。

对于await,我举个例子:假设有两个异步函数async a与async b,其中a代码中某一步有await,当程序碰到关键字await之后,便会挂起a,去执行b,当挂起条件结束,不论b是否执行完毕,都必须马上从b跳出来,去执行a。

更改上面的代码,如下所示:

import time
import asyncio


async def testIO_1():
    print('开始执行IO任务1...')
    await asyncio.sleep(2)  # 假设任务耗时2秒
    print('IO任务1完成,耗时2秒')
    return testIO_1.__name__


async def testIO_2():
    print('开始执行IO任务2...')
    await asyncio.sleep(3)  # 假设任务耗时3秒
    print('IO任务2完成,耗时3秒')
    return testIO_2.__name__


async def main():   # 调用方
    tasks = [testIO_1(), testIO_2()]    # 把任务添加到tasks
    done, pending = await asyncio.wait(tasks)   # 子生成器
    # print(done)
    # print(pending)
    for r in done:  # done和pending都是一个任务,所以返回结果需要逐个调用result()
        print('协程无序返回值:', r.result())


if __name__ == "__main__":
    start = time.time()
    loop = asyncio.get_event_loop() # 创建事件循环对象
    try:
        loop.run_until_complete(main()) # 完成事件循环,直到最后一个任务结束
    finally:
        loop.close()    # 结束事件循环
    print('所有IO任务,总耗时%.5f' % float(time.time() - start))

运行结果,如下所示:

开始执行IO任务2...
开始执行IO任务1...
IO任务1完成,耗时2秒
IO任务2完成,耗时3秒
协程无序返回值: testIO_1
协程无序返回值: testIO_2
所有IO任务,总耗时2.99948

多种方式实现协程

一、通过asyncio.wait()

你可以将一个操作分成多部分并分开执行,wait(tasks)可以被用于中断任务集合(tasks)中的某个被事件循环轮询到的任务,直到协程在后台操作完成之后才被唤醒。

对于上面的代码,我们采用的就是这种方式。

done, pending = await asyncio.wait(tasks)   

此处传入并发运行的协程对象,通过await返回一个包含done与pending的元组,done表示已完成的任务列表,pending表示未完成的任务列表。

二、asyncio.gather()

如果你只关心协程并发运行后的结果集合,可以考虑采用gather(),它不仅通过await返回仅一个结果集,而且这个结果集的结果顺序是传入任务的原始顺序。

具体代码,如下所示:

import time
import asyncio


async def testIO_1():
    print('开始执行IO任务1...')
    await asyncio.sleep(2)  # 假设任务耗时2秒
    print('IO任务1完成,耗时2秒')
    return testIO_1.__name__


async def testIO_2():
    print('开始执行IO任务2...')
    await asyncio.sleep(3)  # 假设任务耗时3秒
    print('IO任务2完成,耗时3秒')
    return testIO_2.__name__


async def main():
    results = await asyncio.gather(testIO_1(), testIO_2())
    print(results)



if __name__ == "__main__":
    start = time.time()
    loop = asyncio.get_event_loop() # 创建事件循环对象
    try:
        loop.run_until_complete(main()) # 完成事件循环,直到最后一个任务结束
    finally:
        loop.close()    # 结束事件循环
    print('所有IO任务,总耗时%.5f' % float(time.time() - start))

运行结果,如下所示:

开始执行IO任务1...
开始执行IO任务2...
IO任务1完成,耗时2秒
IO任务2完成,耗时3秒
['testIO_1', 'testIO_2']
所有IO任务,总耗时3.00510

从运行结果,你会发现它是有序的,而采用wait()的方式是无序的。

三、asyncio.as_completed

as_completed(tasks)是一个生成器,它管理着一个协程列表的运行,当任务集合中的某个任务率先执行完毕时,会通过await关键字返回该任务的结果。可见其返回结果的顺序与wait()相同,均是按照完成任务顺序排列的。

具体代码,如下所示:

import time
import asyncio


async def testIO_1():
    print('开始执行IO任务1...')
    await asyncio.sleep(2)  # 假设任务耗时2秒
    print('IO任务1完成,耗时2秒')
    return testIO_1.__name__


async def testIO_2():
    print('开始执行IO任务2...')
    await asyncio.sleep(3)  # 假设任务耗时3秒
    print('IO任务2完成,耗时3秒')
    return testIO_2.__name__

async def main():
    tasks = [testIO_1(), testIO_2()]
    for completed_task in asyncio.as_completed(tasks):
        results = await completed_task
        print('协程无序返回值', results)



if __name__ == "__main__":
    start = time.time()
    loop = asyncio.get_event_loop() # 创建事件循环对象
    try:
        loop.run_until_complete(main()) # 完成事件循环,直到最后一个任务结束
    finally:
        loop.close()    # 结束事件循环
    print('所有IO任务,总耗时%.5f' % float(time.time() - start))

运行结果,如下所示:

开始执行IO任务2...
开始执行IO任务1...
IO任务1完成,耗时2秒
协程无序返回值 testIO_1
IO任务2完成,耗时3秒
协程无序返回值 testIO_2
所有IO任务,总耗时3.01378

小结

1、同步编程的并发性不高

2、多进程编程效率需要受CPU核数限制,当任务数量远大于CPU核数时,执行效率会降低。

3、多线程需要线程之间的通信,而且需要锁机制来防止共享变量被不同线程乱改,而且由于python中的全局解释锁,所以也无法做到真正的并行。

从上面的程序可以看出,使用as_completed(tasks)和wait(tasks)相同之处是返回结果的顺序是协程的完成顺序,与gather()相反。不同之处就是as_completed(tasks)可以实时返回当前完成的结果,而wait(tasks)需要等待所有协程结束后返回的done去获取结果。

实战 ——妹子图

网页分析

基础部分,我就不再过多的叙述。

最主要的任务就是获取到每一张图片的URL。

主要的库:

  • httpx
  • asyncio
  • lxml
页数

因为图片一共有144页,每页的url地址不同,首先获取每一页的url地址。

def get_page_url():
    page_urls = []
    for i in range(1, 5):
        url = f'https://www.xxx.com/jiepai/comment-page-{i}/'
        page_urls.append(url)
    return page_urls

接下来就通过请求每一页的url地址,并对网页进行解析获取图片的url地址。

def get_img_urls():
    img_urls = []
    page_urls = get_page_url()
    for page_url in page_urls:

        response = httpx.get(page_url, headers=headers)
        html = etree.HTML(response.text)
        lis = html.xpath('//div[@id="comments"]/ul/li')
        print(lis)
        for li in lis:
            data_originals = li.xpath('./div//img/@data-original')
            # print(data_originals)
            # img_urls.extend(data_originals)
            for data_original in data_originals:
                # print(data_original)
                img_urls.append(data_original)

    return img_urls

获取到url之后,只需要创建协程函数即可。

async def save_img(index, img_url):
    print(f'*****正在下载第{index}张*****')
    content = httpx.get(img_url, headers=headers).content
    with open(f'./街拍美女/{index}.jpg', 'wb') as f:
        f.write(content)
def main():
    loop = asyncio.get_event_loop()
    img_urls = get_img_urls()
    print(len(img_urls))
    tasks = [save_img(img[0], img[1]) for img in enumerate(img_urls)]
    try:
        loop.run_until_complete(asyncio.wait(tasks))
    finally:
        loop.close()

效果展示

效果展示

至此,成功获取到了两千多张图片。

本站地址不提供,防止进入小黑屋(需要可以私聊我)

告诉大家一个好消息,本站导航栏中的几个模块我已经写好代码了,只需要套用即可,需要可以私聊。

相关文章

  • 这怕是全网最详细的异步IO之协程详解!

    大家好,我是剑南,今天我为大家带来的内容是python异步IO的协程知识的分享。 为何引出协程 协程是python...

  • Python 协程和异步IO详解

    1. 协程的使用 调用next()时,会产出yield右边的值然后暂停 调用send()时,产出值会赋值给yiel...

  • python异步IO之协程

    异步和同步 什么是异步? 异步就是一项任务不一次性执行完,先执行第一段转而执行其他任务,等做好了准备,再回来执行剩...

  • Python3 asyncio模块学习(1)

    异步协程模块:所谓的协程是线程的弱化版,程序遇到阻塞的IO操作,会自动跳转到后续代码执行,并不等待IO响应,直到有...

  • 【Python入门】49.异步IO之 协程

    摘要:介绍什么是异步IO,什么是协程。在Python中是如何通过Generator实现协程运行的。 *写在前面:为...

  • python3中异步IO

    python2中的gevent通过协程已经实现了异步IO,python3中专门有一个模块来处理异步IO,ascyi...

  • 认识协程

    1、协程是什么? 协程基于线程,它是轻量级线程。协程让异步逻辑同步化,杜绝回调地狱。协程最核心的点就是,函数或者一...

  • 异步IO、协程和爬虫

    补充:多进程和多线程的选择 还记得多进程vs多线程吗?还记得CPU密集型和IO密集型吗?还记得GIL吗? 由于GI...

  • 进程 线程 协程 异步IO

    进程 线程 协程 异步IO 线程和进程对比 使用线程的方式不能很好的使用多核cpu的能力 使用进程可以利用多核CP...

  • Python协程之asyncio

    asyncio 是 Python 中的异步IO库,用来编写并发协程,适用于IO阻塞且需要大量并发的场景,例如爬虫、...

网友评论

    本文标题:这怕是全网最详细的异步IO之协程详解!

    本文链接:https://www.haomeiwen.com/subject/wjmabltx.html