作者:enjoy楠神 | 来源:互联网 | 2024-10-30 18:17
在处理大规模并发请求时,传统的多线程或多进程模型往往无法有效解决性能瓶颈问题。尽管它们在处理小规模任务时能提升效率,但在高并发场景下,系统资源的过度消耗和上下文切换的开销会显著降低整体性能。相比之下,Python的`asyncio`模块通过协程提供了一种轻量级且高效的并发解决方案。本文将深入解析`asyncio`模块的原理及其在实际应用中的优化技巧,帮助开发者更好地利用协程技术提升程序性能。
为什么使用协程?
当多线程或者多进程足够多时,实际上并不能解决性能的瓶颈问题,也就是多线程和多进程对小规模的请求可以提高效率,过多的请求实际上会降低服务资源响应效率,因此协程是更好的解决文案。
什么是协程?
当一个程序遇到阻塞时,如果将这个程序挂起,然后将它的cpu权限拿出来去执行我们的其他程序,执行完后再回过头来执行这些挂起的程序,此时所有非阻塞操作已经执行完毕,最后在一起执行阻塞程序,是不是相当于做了异步。
因此,协程的作用就是检测阻塞的程序,在单进程和单线程的情况下实现异步,相比多线程和多进程效率更高。
asyncio协程模块(python3.5以上)
协程的代码基本构成
-
特殊函数
- 在普通函数前添加一个async关键字,则该函数就变成一个特殊的函数
- 特殊函数的特殊之处是什么?
- 1.特殊函数被调用后,函数内部的程序语句(函数体)没有被立即执行
- 2.特殊函数被调用后,会返回一个协程对象
-
协程:
- 调用特殊函数即创建一个协程对象。
- 因此,协程对象 = 特殊的函数 = 函数体(一组指定形式的操作)
-
任务:
- 任务对象就是一个高级的协程对象,即任务对象可以绑定一个回调函数
- 任务对象 = 协程对象 == 函数体(一组指定形式的操作)
-
事件循环:
- 事件循环对象,,可以将其当做是一个容器,该容器是用来装载任务对象的。创建好了一个或多个任务对象后,将任务对象装载到事件循环中,启动事件循环对象,则其内部装载的任务对象对应的相关操作就会被立即执行。
import asyncio
import time
# 创建特殊函数,一般将有阻塞操作设置特殊函数,在普通函数前加关键字async
async def get_request(url):print('正在请求的网址是:',url)time.sleep(2)print('请求网址结束!')return 123# 自定义一个回调函数(一般来做数据解析),给任务对象使用:必须有一个参数,用来获取特殊函数的返回值
def t_callback(t):#参数t就是任务对象data = t.result() #result()函数就可以返回特殊函数内部的返回值print('获取到特殊函数的返回值为:',data)# 创建协程对象
c = get_request('www.123.com')# 创建任务对象
task = asyncio.ensure_future(c)# 任务对象添加回调函数,事件对象启用的时候,特殊函数和回调函数按顺序执行
task.add_done_callback(t_callback)#创建事件循环对象
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
多任务的协程
特殊函数内部,不可以出现不支持异步模块的代码,否则会中断整个异步效果,例如sleep,requests,可以通过执行程序来判断。具体操作如下:
添加多个任务需要使用:wait()函数,其他代码省略,看最后一步
'''
特殊函数
回调函数
协程对象
'''tasks = []
for url in urls:c = get_request(url)task = asyncio.ensure_future(c)task.add_done_callback(parse)tasks.append(task)
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
await关键字:挂起发生阻塞操作的任务对象。在任务对象表示的操作中,凡是阻塞操作的前面都必须加上await关键字进行修饰,但不是所有阻塞操作都可以加await,需要添加支持协程的阻塞操作,await才会生效async def get_request(url):print('正在请求:',url)await asyncio.sleep(2)print('请求结束:',url)
完整代码
# 特殊函数先写出基本的网络请求框架,然后在每个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)
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))