1. 协程概念
协程,也可以被称为微线程,是一种用户态内的上下文切换技术。简而言之,就是通过一个线程实现代码块的相互切换执行。
进程和线程是内核级别的程序,而协程是函数级别的程序,是可以通过人为调用函数来实现的,核心机制就是 事件循环。
- 事件循环
通过注册事件与事件处理函数。当发生了特定事件后,通过循环来知晓事件的发生,并调用注册的处理函数。总结起来就是三个步骤:注册、监听、处理。 - 任务状态
协程的任务状态有阻塞、就绪、运行。当一协程在运行状态,遇到了I/O操作,便会到事件循环中监听I/O操作完成了的事件,并注册自身的上下文以及自身的唤醒函数,之后该协程就变为阻塞状态。当监听的事件发生后,阻塞状态的协程就会被唤醒,进入就绪状态,将在下一次事件循环继续运行。
2. greenlet 实现协程
pip install greenlet
from greenlet import greenlet
def func1():
print(1) # 第二步:输出1
gr2.switch() # 第三步:切换到func2函数
print(2) # 第六步:输出2
gr2.switch() # 第七步:切换到func2函数,从上一次执行的位置继续往后执行
def func2():
print(3) # 第四步:输出3
gr1.switch() # 第五步:切换到func1函数,从上一次执行的位置继续往后执行
gr1 = greenlet(func1)
gr2 = greenlet(func2)
gr1.switch() # 第一步:去执行func1函数
3. @asyncio.coroutine 和 yield from 实现协程
yield from 关键字用于被asyncio装饰的函数时,即可实现协程。在协程中,只要和IO任务类似的、耗费时间的任务都需要使用yield from 来进行中断,达到异步功能。
import asyncio
@asyncio.coroutine
def func1():
print(1)
yield from asyncio.sleep(2) # 使用yield from来进行任务切换
print(2)
@asyncio.coroutine
def func2():
print(3)
yield from asyncio.sleep(2)
print(4)
tasks = [
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2()),
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
可以把主流程封装成一个协程函数去执行:
import asyncio
@asyncio.coroutine
def func1():
print(1)
yield from asyncio.sleep(2) # 只要和io任务类似的、耗费时间的任务,都需要使用yield from来进行中断
print(2)
return func1.__name__
@asyncio.coroutine
def func2():
print(3)
yield from asyncio.sleep(2)
print(4)
return func2.__name__
@asyncio.coroutine
def main():
tasks = [func1(), func2()]
done, pending = yield from asyncio.wait(tasks) # done:已完成的任务列表;pending:未完成的任务列表
for i in done:
print(f"协程无序返回值:{i.result()}")
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
4. async & await 关键字实现协程
通过 async & await 来将一个函数分成多个段,而这些段都是协程里面调度的单位,粒度比进程、线程都要小。只需要简单改造两步:
1. 把 @asyncio.coroutine 替换成 async
2. 把 yield from 替换成 await
import asyncio
async def func1():
print(1)
await asyncio.sleep(2) # 在io或者耗时的操作,加上await
print(2)
return func1.__name__
async def func2():
print(3)
await asyncio.sleep(2)
print(4)
return func2.__name__
task1 = asyncio.create_task(func1())
task2 = asyncio.create_task(func2())
tasks = [task1, task2]
# loop = asyncio.get_event_loop()
# loop.run_until_complete(asyncio.wait(tasks))
# loop.close()
done, pending = asyncio.run(asyncio.wait(tasks)) # 上面注释的三行和这一行是等价的
同样可以把主流程封装成一个协程函数去执行:
import asyncio
async def func1():
print(1)
await asyncio.sleep(2)
print(2)
return func1.__name__
async def func2():
print(3)
await asyncio.sleep(2)
print(4)
return func2.__name__
async def main():
task1 = asyncio.create_task(func1())
task2 = asyncio.create_task(func2())
tasks = [task1, task2]
done, pending = await asyncio.wait(tasks)
for i in done:
print(f"协程无序返回值:{i.result()}")
asyncio.run(main())
5. 效率更高的uvloop
uvloop是asyncio事件循环的替代方案,在效率上uvloop事件循环 > python默认的asyncio事件循环。
pip install uvloop
如何替换使用?
import uvloop
import asyncio
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
# 编写asyncio的代码,和之前的一样
async def main():
...
# 内部的事件循环会自动变成uvloop
asyncio.run(main())
5. 协程案例
- asyncio+支持异步操作的模块
操作redis的异步模块:aioredis
pip install aioredis==1.3.1
import asyncio
import aioredis # 版本 aioredis==1.3.1
async def execute(address):
print("start to connect")
# 网络io操作:创建redis连接
redis = await aioredis.create_redis(address)
# 网络IO:redis设置哈希值
await redis.hmset_dict("cars", key1=1, key2=2, key3=3)
# 网络IO:redis取值
result = await redis.hgetall("car", encoding="utf-8")
# 关闭连接
redis.close()
# 关闭连接池
await redis.wait_closed()
url_list = [execute(i) for i in ["redis://192.168.3.40:6379", "redis://192.168.3.41:6379"]]
asyncio.run(asyncio.wait(url_list))
操作mysql的异步模块:aiomysql
import asyncio
import aiomysql
async def execute(host, passwd):
print("start to connect")
# 网络IO:先去连接ip1,遇到IO后自动切换任务,去连接ip2
conn = await aiomysql.connect(host=host, port=3306, user="root", password=passwd, db="mysql")
cur = await conn.cursor()
await cur.execute("select * from user")
userdata = await cur.fetchall()
print(userdata)
await cur.close()
conn.close()
print("connect over")
url_list = [execute("192.168.3.40", "root"), execute("192.168.3.41", "root")]
asyncio.run(asyncio.wait(url_list))
访问网路的异步模块:aiohttp
# 用aiohttp异步模块访问网站
import asyncio
import aiohttp
async def spider(session, url):
print(f"web page url: {url}")
async with session.get(url, verify_ssl=False) as response: # 发送请求
text = await response.text()
print(f"page {url} result: {len(text)}")
return text
async def main():
async with aiohttp.ClientSession() as session:
urls = [
"https://python.org",
"https://pypi.org/",
"https://spark.apache.org/"
]
tasks = [spider(session, url) for url in urls]
done, pending = await asyncio.wait(tasks)
return done
if __name__ == '__main__':
asyncio.run(main())
- asyncio + 不支持异步操作的模块
# 不使用aiohttp,去访问网站,和aiohttp做个对比
import asyncio
import requests
async def spider(url):
print("start main")
loop = asyncio.get_event_loop()
# loop.run_in_executor(None, requests.get, url) :第一个参数为进程池或线程池对象,根据业务自定义,None表示使用默认的线程池。
# 这行代码做了什么事?
# 1. 先调用 ThreadPoolExecutor的submit方法,去线程池中申请一个线程去执行requests.get函数,并返回 concurrent.futures.Future对象(这种对象不支持await语法);
# 2. 调用asyncio.warp_future将concurrent.futures.Future对象包装成asynci.Future对象,使其支持await语法。
futu = loop.run_in_executor(None, requests.get, url)
resp = await futu
print(f"{url}: {resp}")
async def main():
urls = [
"https://python.org",
"https://pypi.org/",
"https://spark.apache.org/"
]
tasks = [spider(url) for url in urls]
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())
在上面的基础上,使用自定义的进程池或者线程池去访问网站
import asyncio
import requests
async def spider_user_thread_pool(thread_pool, url):
"""使用自定义的线程池"""
loop = asyncio.get_event_loop()
futu = loop.run_in_executor(thread_pool, requests.get, url)
resp = await futu
print(f"{url}: {resp}")
async def main():
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
user_thread_pool = ThreadPoolExecutor(max_workers=2)
urls = [
"https://python.org",
"https://pypi.org/",
"https://spark.apache.org/"
]
tasks = [spider_user_thread_pool(user_thread_pool, url) for url in urls]
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())