python协程与 asyncio 库详情

目录
  • 1.asyncio 异步 I/O 库
    • 异步函数的定义
    • 事件循环 event_loop
    • 创建 task
    • 回调返回值
    • 循环事件关闭
  • 2.本节爬虫项目

前言:

python 中协程概念是从 3.4 版本增加的,但 3.4 版本采用是生成器实现,为了将协程和生成器的使用场景进行区分,使语义更加明确,在 python 3.5 中增加了 async 和 await 关键字,用于定义原生协程。

1.asyncio 异步 I/O 库

python 中的 asyncio 库提供了管理事件、协程、任务和线程的方法,以及编写并发代码的原语,即 async 和 await

该模块的主要内容:

  • 事件循环:event_loop,管理所有的事件,是一个无限循环方法,在循环过程中追踪事件发生的顺序将它们放在队列中,空闲时则调用相应的事件处理者来处理这些事件;
  • 协程:coroutine,子程序的泛化概念,协程可以在执行期间暂停,等待外部的处理(I/O 操作)完成之后,再从暂停的地方继续运行,函数定义式使用 async关键字,这样这个函数就不会立即执行,而是返回一个协程对象;
  • FutureTaskFuture对象表示尚未完成的计算,Task是 Future的子类,包含了任务的各个状态,作用是在运行某个任务的同时可以并发的运行多个任务。

异步函数的定义

异步函数本质上依旧是函数,只是在执行过程中会将执行权交给其它协程,与普通函数定义的区别是在 def关键字前增加 async

# 异步函数
import asyncio
# 异步函数
async def func(x):
    print("异步函数")
    return x ** 2
ret = func(2)
print(ret)

运行代码输入如下内容:

sys:1: RuntimeWarning: coroutine 'func' was never awaited
<coroutine object func at 0x0000000002C8C248>

函数返回一个协程对象,如果想要函数得到执行,需要将其放到事件循环 event_loop中。

事件循环 event_loop

event_loop是 asyncio模块的核心,它将异步函数注册到事件循环上。 过程实现方式为:由 loop在适当的时候调用协程,这里使用的方式名为 asyncio.get_event_loop(),然后由 run_until_complete(协程对象) 将协程注册到事件循环中,并启动事件循环。

import asyncio
# 异步函数
async def func(x):
    print("异步函数")
    return x ** 2
# 协程对象,该对象不能直接运行
coroutine1 = func(2)
# 事件循环对象
loop = asyncio.get_event_loop()
# 将协程对象加入到事件循环中,并执行
ret = loop.run_until_complete(coroutine1)
print(ret)

首先在 python 3.7 之前的版本中使用异步函数是安装上述流程:

  • 先通过 asyncio.get_event_loop()获取事件循环loop对象;
  • 然后通过不同的策略调用 loop.run_until_complete()或者loop.run_forever()执行异步函数。

在 python 3.7 之后的版本,直接使用 asyncio.run() 即可,该函数总是会创建一个新的事件循环并在结束时进行关闭。

最新的官方文档都采用的是run方法。 官方案例

import asyncio
async def main():
    print('hello')
    await asyncio.sleep(1)
    print('world')
asyncio.run(main())

接下来在查看一个完整的案例,并且结合await关键字。

import asyncio
import time
# 异步函数1
async def task1(x):
    print("任务1")
    await asyncio.sleep(2)
    print("恢复任务1")
    return x
# 异步函数2
async def task2(x):
    print("任务2")
    await asyncio.sleep(1)
    print("恢复任务2")
    return x
async def main():
    start_time = time.perf_counter()
    ret_1 = await task1(1)
    ret_2 = await task2(2)
    print("任务1 返回的值是", ret_1)
    print("任务2 返回的值是", ret_2)
    print("运行时间", time.perf_counter() - start_time)
if __name__ == '__main__':
	# 创建一个事件循环
    loop = asyncio.get_event_loop()
    # 将协程对象加入到事件循环中,并执行
    loop.run_until_complete(main())

代码输出如下所示:

任务1
恢复任务1
任务2
恢复任务2
任务1 返回的值是 1
任务2 返回的值是 2
运行时间 2.99929154

上述代码创建了 3 个协程,其中 task1和 task2都放在了协程函数 main中,I/O 操作通过 asyncio.sleep(1)进行模拟,整个函数运行时间为 2.9999 秒,接近 3 秒,依旧是串行进行,如果希望修改为并发执行,将代码按照下述进行修改。

import asyncio
import time
# 异步函数1
async def task1(x):
    print("任务1")
    await asyncio.sleep(2)
    print("恢复任务1")
    return x
# 异步函数2
async def task2(x):
    print("任务2")
    await asyncio.sleep(1)
    print("恢复任务2")
    return x
async def main():
    start_time = time.perf_counter()
    ret_1,ret_2 = await asyncio.gather(task1(1),task2(2))
    print("任务1 返回的值是", ret_1)
    print("任务2 返回的值是", ret_2)
    print("运行时间", time.perf_counter() - start_time)
if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

上述代码最大的变化是将task1task2放到了asyncio.gather()中运行,此时代码输出时间明显变短。

任务1
任务2
恢复任务2 # 任务2 由于等待时间短,先返回。
恢复任务1
任务1 返回的值是 1
任务2 返回的值是 2
运行时间 2.0005669480000003

asyncio.gather()可以更换为asyncio.wait()修改代码如下所示:

import asyncio
import time
# 异步函数1
async def task1(x):
    print("任务1")
    await asyncio.sleep(2)
    print("恢复任务1")
    return x
# 异步函数2
async def task2(x):
    print("任务2")
    await asyncio.sleep(1)
    print("恢复任务2")
    return x
async def main():
    start_time = time.perf_counter()
    done, pending = await asyncio.wait([task1(1), task2(2)])
    print(done)
    print(pending)
    print("运行时间", time.perf_counter() - start_time)
if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

asyncio.wait()返回一个元组,其中包含一个已经完成的任务集合,一个未完成任务的集合。

gather 和 wait 的区别:

  • gather:需要所有任务都执行结束,如果任意一个协程函数崩溃了,都会抛异常,不会返回结果;
  • wait:可以定义函数返回的时机,可以设置为 FIRST_COMPLETED(第一个结束的), FIRST_EXCEPTION(第一个出现异常的), ALL_COMPLETED(全部执行完,默认的)。
done,pending = await asyncio.wait([task1(1),task2(2)],return_when=asyncio.tasks.FIRST_EXCEPTION)

创建 task

由于协程对象不能直接运行,在注册到事件循环时,是run_until_complete方法将其包装成一个 task对象。该对象是对coroutine对象的进一步封装,它比coroutine对象多了运行状态,例如 pendingrunningfinished,可以利用这些状态获取协程对象的执行情况。

下面显示的将coroutine对象封装成task对象,在上述代码基础上进行修改。

import asyncio
import time
# 异步函数1
async def task1(x):
    print("任务1")
    await asyncio.sleep(2)
    print("恢复任务1")
    return x
# 异步函数2
async def task2(x):
    print("任务2")
    await asyncio.sleep(1)
    print("恢复任务2")
    return x
async def main():
    start_time = time.perf_counter()
    # 封装 task 对象
    coroutine1 = task1(1)
    task_1 = loop.create_task(coroutine1)
    coroutine2 = task2(2)
    task_2 = loop.create_task(coroutine2)
    ret_1, ret_2 = await asyncio.gather(task_1, task_2)
    print("任务1 返回的值是", ret_1)
    print("任务2 返回的值是", ret_2)
    print("运行时间", time.perf_counter() - start_time)
if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

由于task对象是future对象的子类对象,所以上述代码也可以按照下述内容修改:

# task_2 = loop.create_task(coroutine2)
task_2 = asyncio.ensure_future(coroutine2)

下面将task对象的各个状态进行打印输出。

import asyncio
import time
# 异步函数1
async def task1(x):
    print("任务1")
    await asyncio.sleep(2)
    print("恢复任务1")
    return x
# 异步函数2
async def task2(x):
    print("任务2")
    await asyncio.sleep(1)
    print("恢复任务2")
    return x
async def main():
    start_time = time.perf_counter()
    # 封装 task 对象
    coroutine1 = task1(1)
    task_1 = loop.create_task(coroutine1)
    coroutine2 = task2(2)
    # task_2 = loop.create_task(coroutine2)
    task_2 = asyncio.ensure_future(coroutine2)
    # 进入 pending 状态
    print(task_1)
    print(task_2)
    # 获取任务的完成状态
    print(task_1.done(), task_2.done())
    # 执行任务
    await task_1
    await task_2
    # 再次获取完成状态
    print(task_1.done(), task_2.done())
    # 获取返回结果
    print(task_1.result())
    print(task_2.result())
    print("运行时间", time.perf_counter() - start_time)
if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

await task_1表示的是执行该协程,执行结束之后,task.done()返回 Truetask.result()获取返回值。

回调返回值

当协程执行完毕,需要获取其返回值,刚才已经演示了一种办法,使用 task.result()方法获取,但是该方法仅当协程运行完毕时,才能获取结果,如果协程没有运行完毕,result()方法会返回 asyncio.InvalidStateError(无效状态错误)。

一般编码都采用第二种方案,通过add_done_callback()方法绑定回调。

import asyncio
import requests
async def request_html():
    url = 'https://www.csdn.net'
    res = requests.get(url)
    return res.status_code
def callback(task):
    print('回调:', task.result())
loop = asyncio.get_event_loop()
coroutine = request_html()
task = loop.create_task(coroutine)
# 绑定回调
task.add_done_callback(callback)
print(task)
print("*"*100)
loop.run_until_complete(task)
print(task)

上述代码当coroutine执行完毕时,会调用callback函数。

如果回调函数需要多个参数,请使用functools模块中的偏函数(partial)方法

循环事件关闭

建议每次编码结束之后,都调用循环事件对象close()方法,彻底清理loop对象。

2.本节爬虫项目

本节课要采集的站点由于全部都是 coser 图片,所以地址在代码中查看即可。

完整代码如下所示:

import threading
import asyncio
import time
import requests
import lxml
from bs4 import BeautifulSoup
async def get(url):
    return requests.get(url)
async def get_html(url):
    print("准备抓取:", url)
    res = await get(url)
    return res.text
async def save_img(img_url):
    # thumbMid_5ae3e05fd3945 将小图替换为大图
    img_url = img_url.replace('thumb','thumbMid')
    img_url = "http://mycoser.com/" + img_url
    print("图片下载中:", img_url)
    res = await get(img_url)
    if res is not None:
        with open(f'./imgs/{time.time()}.jpg', 'wb') as f:
            f.write(res.content)
            return img_url,"ok"
async def main(url_list):
    # 创建 5 个任务
    tasks = [asyncio.ensure_future(get_html(url_list[_])) for _ in range(len(url_list))]
    dones, pending = await asyncio.wait(tasks)
    for task in dones:
        html = task.result()
        soup = BeautifulSoup(html, 'lxml')
        divimg_tags = soup.find_all(attrs={'class': 'workimage'})
        for div in divimg_tags:
            ret = await save_img(div.a.img["data-original"])
            print(ret)
if __name__ == '__main__':
    urls = [f"http://mycoser.com/picture/lists/p/{page}" for page in range(1, 17)]
    totle_page = len(urls) // 5 if len(urls) % 5 == 0 else len(urls) // 5 + 1
    # 对 urls 列表进行切片,方便采集
    for page in range(0, totle_page):
        start_page = 0 if page == 0 else page * 5
        end_page = (page + 1) * 5
        # 循环事件对象
        loop = asyncio.get_event_loop()
        loop.run_until_complete(main(urls[start_page:end_page]))

代码说明:上述代码中第一个要注意的是await关键字后面只能跟如下内容:

  • 原生的协程对象;
  • 一个包含await方法的对象返回的一个迭代器。

所以上述代码get_html函数中嵌套了一个协程 get。主函数 main里面为了运算方便,直接对 urls 进行了切片,然后通过循环进行运行。

当然上述代码的最后两行,可以直接修改为:

 # 循环事件对象
 # loop = asyncio.get_event_loop()
 #
 # loop.run_until_complete(main(urls[start_page:end_page]))
 asyncio.run(main(urls[start_page:end_page]))

轻松获取一堆高清图片:

到此这篇关于python协程与 asyncio 库详情的文章就介绍到这了,更多相关python 协程内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅谈Python协程asyncio

    一.协程 官方描述; 协程是子例程的更一般形式. 子例程可以在某一点进入并在另一点退出. 协程则可以在许多不同的点上进入.退出和恢复. 它们可通过 async def 语句来实现. 参见 PEP 492. 协程不是计算机内部提供的,不像进程.线程,由电脑本身提供,它是由程序员人为创造的, 实现函数异步执行. 协程(Coroutine),也可以被称为微线程,是一种用户太内的上下文切换技术,其实就是通过一个线程实现代码块相互切换执行.看上去像子程序,但执行过程中,在子程序内部可中断,然后转而执行别的

  • python 中的 asyncio 异步协程

    目录 一.定义协程 二.运行协程 三.协程回调 四.运行多个协程 五.run_forever 六.多协程中关闭run_forever 一.定义协程 asyncio 执行的任务,称为协程,但是Asyncio 并不能带来真正的并行 Python 的多线程因为 GIL(全局解释器锁)的存在,也不能带来真正的并行 import asyncio # 通过 async 定义一个协程 async def task(): print('这是一个协程') # 判断是否是一个协程,返回True print(asyn

  • Python协程asyncio 异步编程笔记分享

    目录 1.事件循环 2.协程和异步编程 2.1 基本使用 2.2 await 2.3 Task对象 1.事件循环 可以理解成为一个死循环,去检查任务列表中的任务,如果可执行就去执行,如果检查不到就是不可执行的,那就忽略掉去执行其他可执行的任务,如果IO结束了(比如说去百度下载图片,下载完了就会变成可执行任务)再去执行下载完成之后的逻辑 #这里的任务是有状态的,比如这个任务已经完成或者正在执行或者正在IO等待 任务列表 = [ 任务1, 任务2, 任务3,... ] while True: 可执行

  • python asyncio 协程库的使用

    asyncio 是 python 力推多年的携程库,与其 线程库 相得益彰,更轻量,并且协程可以访问同一进程中的变量,不需要进程间通信来传递数据,所以使用起来非常顺手. asyncio 官方文档写的非常简练和有效,半小时内可以学习和测试完,下面为我的一段 HelloWrold,感觉可以更快速的帮你认识 协程 . 定义协程 import asyncio import time async def say_after(delay, what): await asyncio.sleep(delay)

  • Python协程asyncio模块的演变及高级用法

    Python协程及asyncio基础知识 协程(coroutine)也叫微线程,是实现多任务的另一种方式,是比线程更小的执行单元,一般运行在单进程和单线程上.因为它自带CPU的上下文,它可以通过简单的事件循环切换任务,比进程和线程的切换效率更高,这是因为进程和线程的切换由操作系统进行. Python实现协程的主要借助于两个库:asyncio和gevent.由于asyncio已经成为python的标准库了无需pip安装即可使用,这意味着asyncio作为Python原生的协程实现方式会更加流行.本

  • python 使用事件对象asyncio.Event来同步协程的操作

    事件对象asyncio.Event是基于threading.Event来实现的. 事件可以一个信号触发多个协程同步工作, 例子如下: import asyncio import functools def set_event(event): print('setting event in callback') event.set() async def coro1(event): print('coro1 waiting for event') await event.wait() print(

  • python协程与 asyncio 库详情

    目录 1.asyncio 异步 I/O 库 异步函数的定义 事件循环 event_loop 创建 task 回调返回值 循环事件关闭 2.本节爬虫项目 前言: python 中协程概念是从 3.4 版本增加的,但 3.4 版本采用是生成器实现,为了将协程和生成器的使用场景进行区分,使语义更加明确,在 python 3.5 中增加了 async 和 await 关键字,用于定义原生协程. 1.asyncio 异步 I/O 库 python 中的 asyncio 库提供了管理事件.协程.任务和线程的

  • Python协程asyncio异步编程笔记分享

    目录 1.事件循环 2.协程和异步编程 2.1基本使用 2.2await 2.3Task对象 1.事件循环 可以理解成为一个死循环,去检查任务列表中的任务,如果可执行就去执行,如果检查不到就是不可执行的,那就忽略掉去执行其他可执行的任务,如果IO结束了(比如说去百度下载图片,下载完了就会变成可执行任务)再去执行下载完成之后的逻辑 #这里的任务是有状态的,比如这个任务已经完成或者正在执行或者正在IO等待 任务列表 = [ 任务1, 任务2, 任务3,... ] while True: 可执行的任务

  • 深入浅析python 协程与go协程的区别

    进程.线程和协程 进程的定义: 进程,是计算机中已运行程序的实体.程序本身只是指令.数据及其组织形式的描述,进程才是程序的真正运行实例. 线程的定义: 操作系统能够进行运算调度的最小单位.它被包含在进程之中,是进程中的实际运作单位. 进程和线程的关系: 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务. CPU的最小调度单元是线程不是进程,所以单进程多线程也可以利用多核CPU. 协程的定义: 协程通过在线程中实现调度,避免了陷入内核级别的上下文切换

  • Python 协程与 JavaScript 协程的对比

    目录 1.前言 2.什么是协程? 3.混乱的历史 3.1 Python 协程的进化 4.JavaScript 协程的进化 5.Python 协程成熟体 5.1 协程(coroutine) 5.2 任务(Task 对象) 5.3 未来对象(Future) 5.4几种事件循环(event loop) 6.JavaScript 协程成熟体 6.1Promise 继续使用 6.2 async.await语法糖 6.3 js 异步执行的运行机制 6.4 event loop 将任务划分 7.总结与对比 1

  • Python协程实践分享

    目录 协程 yield在协程中的用法 经典示例 生产者-消费者模式(协程) gevent第三方库协程支持 经典代码 asyncio内置库协程支持 关于aiohttp 协程 协程简单来说就是一个更加轻量级的线程,并且不由操作系统内核管理,完全由程序所控制(在用户态执行).协程在子程序内部是可中断的,然后转而执行其他子程序,在适当的时候返回过来继续执行. 协程的优势?(协程拥有自己的寄存器上下文和栈,调度切换时,寄存器上下文和栈保存到其他地方,在切换回来的时候,恢复先前保存的寄存器上下文和栈,直接操

  • Python协程的用法和例子详解

    从句法上看,协程与生成器类似,都是定义体中包含 yield 关键字的函数.可是,在协程中, yield 通常出现在表达式的右边(例如, datum = yield),可以产出值,也可以不产出 -- 如果 yield 关键字后面没有表达式,那么生成器产出 None. 协程可能会从调用方接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方法,而不是next(-) 函数. ==yield 关键字甚至还可以不接收或传出数据.不管数据如何流动, yield 都是一种流程控制工具,使用

  • 浅析python协程相关概念

    这篇文章是读者朋友的python协程的学习经验之谈,以下是全部内容: 协程的历史说来话长,要从生成器开始讲起. 如果你看过我之前的文章python奇遇记:迭代器和生成器 ,对生成器的概念应该很了解.生成器节省内存,用的时候才生成结果. # 生成器表达式 a = (x*x for x in range(10)) # next生成值 next(a()) # 输出0 next(a()) # 输出1 next(a()) # 输出4 与生成器产出数据不同的是,协程在产出数据的同时还可以接收数据,具体来说就

随机推荐