hengdin

导航

 

1. 为什么使用协程?

当多线程或者多进程足够多时,实际上并不能解决性能的瓶颈问题,也就是多线程和多进程对小规模的请求可以提高效率,过多的请求实际上会降低服务资源响应效率,因此协程是更好的解决文案。

2. 什么是协程?

当一个程序遇到阻塞时,如果将这个程序挂起,然后将它的cpu权限拿出来去执行我们的其他程序,执行完后再回过头来执行这些挂起的程序,此时所有非阻塞操作已经执行完毕,最后在一起执行阻塞程序,是不是相当于做了异步。

因此,协程的作用就是检测阻塞的程序,在单进程和单线程的情况下实现异步,相比多线程和多进程效率更高。

3. 协程的代码基本构成

asyncio协程模块(python3.5以上)
    1. 特殊函数

在普通函数前添加一个async关键字,则该函数就变成一个特殊的函数

特殊函数的特殊之处是什么?

1.特殊函数被调用后,函数内部的程序语句(函数体)没有被立即执行
2.特殊函数被调用后,会返回一个协程对象

    1. 协程:

调用特殊函数即创建一个协程对象。
因此,协程对象 = 特殊的函数 = 函数体(一组指定形式的操作)

    1. 任务:

任务对象就是一个高级的协程对象,即任务对象可以绑定一个回调函数
任务对象 = 协程对象 == 函数体(一组指定形式的操作)

    1. 事件循环:

事件循环对象,,可以将其当做是一个容器,该容器是用来装载任务对象的。创建好了一个或多个任务对象后,将任务对象装载到事件循环中,启动事件循环对象,则其内部装载的任务对象对应的相关操作就会被立即执行。

示例:

import time
import asyncio


# 创建特殊函数,一般将有阻塞操作设置特殊函数,在普通函数前加关键字async
async def request(url):
    print(f"请求链接:{url}")
    time.sleep(2) 
    print("获取到了resp")
    return url

# 自定义一个回调函数(一般来做数据解析),给任务对象使用:必须有一个参数,用来获取特殊函数的返回值
def parse(res):
    #参数 res 就是任务对象
    res = res.result()  #result()函数就可以返回特殊函数内部的返回值
    print(f"解析到了 resp:{res}")


if __name__ == "__main__":
    
    # 创建协程对象
    req = request("123")
    # 创建任务对象
    task = asyncio.ensure_future(req)
    # 任务对象添加回调函数,事件对象启用的时候,特殊函数和回调函数按顺序执行
    task.add_done_callback(parse)
    
    #创建事件循环对象
    loop = asyncio.get_event_loop()
    loop.run_until_complete(task)

4. 多任务的协程

特殊函数内部,不可以出现不支持异步模块的代码,否则会中断整个异步效果,例如sleep,requests,可以通过执行程序来判断。具体操作如下:

import time
import asyncio

async def request(url):
    print(f"请求链接:{url}")
    # await关键字:挂起发生阻塞操作的任务对象。在任务对象表示的操作中,凡是阻塞操作的前面都必须加上await关键字进行修饰,
    # 但不是所有阻塞操作都可以加await,需要添加支持协程的阻塞操作,await才会生效
    await asyncio.sleep(2)
    print("获取到了resp")
    return url

def parse(res):
    res = res.result()
    print(f"结果为 {res}")


if __name__ == "__main__":
    urls = [str(i) for i in range(10)]
    tasks = []
    for url in urls:
        req = request(url)
        task = asyncio.ensure_future(req)
        task.add_done_callback(parse)
        tasks.append(task)
    
    loop = asyncio.get_event_loop()
    # 添加多个任务需要使用:wait()函数,
    loop.run_until_complete(asyncio.wait(tasks))

注意:

await关键字:挂起发生阻塞操作的任务对象。在任务对象表示的操作中,凡是阻塞操作的前面都必须加上await关键字进行修饰,但不是所有阻塞操作都可以加await,需要添加支持协程的阻塞操作,await才会生效
 
async def get_request(url):
    print('正在请求:',url)
    await asyncio.sleep(2)
    print('请求结束:',url)

5. aiohttp 示例:

# 特殊函数先写出基本的网络请求框架,然后在每个with前面加async,每个阻塞操作前await,便于看懂和记忆
# 使用with是为了关闭协程,避免浪费资源
import asyncio
import time
from lxml import etree
import aiohttp
start = time.time()
urls = [
    'https://www.baidu.com',
    'https://www.baidu.com',
    'https://www.baidu.com'
]
#该任务是用来对指定url发起请求,获取响应数据
async def get_request(url):
    # requests是不支持异步的模块,所以加了await也没用
    # response = await requests.get(url=url)
 
    #aiohttp是支持协程的网络请求,跟requests类似,创建请求对象(aiohttp_requests)
    async with aiohttp.ClientSession() as aiohttp_requests:
        #get请求,常用参数:url,headers,params,proxy
        #post请求,常用参数:url,headers,data,proxy
        #aiohttp处理代理的参数和requests不一样(注意),此处处理代理使用proxy='http://ip:port'
        async with await aiohttp_requests.get(url=url) as response:
            page_text = await response.text()
            #text():获取字符串形式的响应数据
            #read():获取二进制形式的响应数据
            await asyncio.sleep(2)
            return page_text
def call_back(t):#回调函数专门用于数据解析
    #获取任务对象请求到的页面源码数据
    page_text = t.result()
    tree = etree.HTML(page_text)
    a = tree.xpath('//a[1]/@href')
    print(a)
 
tasks = []
for url in urls:
    c = get_request(url)
    task = asyncio.ensure_future(c)
    task.add_done_callback(call_back)
    tasks.append(task)
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
 
print('总耗时:',time.time()-start)

6. uvloop加速

uvloop基于libuv,libuv是一个使用C语言实现的高性能异步I/O库,uvloop用来代替asyncio默认事件循环,可以进一步加快异步I/O操作的速度。

import uvloop
loop = asyncio.get_event_loop()
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) # 在启用事件前加这一行代码即可
loop.run_until_complete(asyncio.wait(tasks))

7.

在 Python3.6 中,需要手动获取事件循环并加入协程任务:

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

在 Python3.7+ 中,运行这个 asyncio 程序如下: ,

asyncio.run(main())

事件循环就是一个循环队列,对其中的协程进行调度执行,当把一个协程加入循环,这个协程创建的其他协程都会自动加入到当前事件循环中。

其实协程对象也不是直接运行,而是被封装成一个个待执行的 Task ,大多数情况下 asyncio 会帮我们进行封装,我们也可以提前自行封装 Task 来获得对协程更多的控制权,注意,封装 Task 需要 当前线程有正在运行的事件循环,否则将引 RuntimeError,这也就是官方建议使用主入口协程的原因,如果在主入口协程之外创建任务就需要先手动获取事件循环然后使用底层方法 loop.create_task(),而在主入口协程之内是一定有正在运行的循环的。任务创建后便有了状态,可以查看运行情况,查看结果,取消任务等:

async def main():
    task = asyncio.create_task(work())
    print(task)
    await task
    print(task)

#----执行结果----#
<Task pending name='Task-2' coro=<work() running at d:	mpcodeasy.py:5>>
<Task finished name='Task-2' coro=<work() done, defined at d:	mpcodeasy.py:5> result=None>

asyncio.create_task() 是 Python3.7 加入的高层级API,在 Python3.6,需要使用低层级API asyncio.ensure_future() 来创建 Future,Future 也是一个管理协程运行状态的对象,与 Task 没有本质上的区别。
2. 并发协程

通常,一个含有一系列并发协程的程序写法如下(Python3.7+):

import asyncio
import time


async def work(num: int):
    '''
    一个工作协程,接收一个数字,将它 +1 后返回
    '''
    print(f'working {num} ...')
    await asyncio.sleep(1)    # 模拟耗时的IO操作
    print(f'{num} -> {num+1} done')
    return num + 1


async def main():
    '''
    主协程,创建一系列并发协程并运行它们
    '''
    # 任务队列
    tasks = [work(num) for num in range(0, 5)]
    # 并发执行队列中的协程并等待结果返回
    results = await asyncio.gather(*tasks)
    print(results)


if __name__ == "__main__":
    asyncio.run(main())

并发运行多个协程任务的关键就是 asyncio.gather(*tasks),它接受多个协程任务并将它们加入到事件循环,所有任务都运行完成后会返回结果列表,这里我们也没有手动封装 Task,因为 gather 函数会自动封装。

并发运行还有另一个方法 asyncio.wait(tasks),它们的区别是:

gather 比 wait 更加高层,gather 可以将任务分组,一般优先使用 gather:
tasks1 = [work(num) for num in range(0, 5)]
tasks2 = [work(num) for num in range(5, 10)]
group1 = asyncio.gather(*tasks1)
group2 = asyncio.gather(*tasks2)
results1, results2 = await asyncio.gather(group1, group2)
print(results1, results2)

在某些定制化任务需求的时候,可以使用 wait:

Python3.8 版本后,直接向 wait() 传入协程对象已弃用,必须手动创建 Task

tasks = [asyncio.create_task(work(num)) for num in range(0, 5)]
done, pending = await asyncio.wait(tasks)
for task in tasks:
    if task in done:
        print(task.result())
for p in pending:
    p.cancel()
posted on 2022-09-29 15:41  hengdin  阅读(212)  评论(0)    收藏  举报