多任务编程协程
协程,又称微线程,纤程。英文名Coroutine。从技术的角度来说,“协程就是你可以暂停执行的函数”。
def func1():
print(1)
...
print(2)
def func2():
print(3)
...
print(4)
func1()
func2()
上述代码是普通的函数定义和执行,按流程分别执行两个函数中的代码,并先后会输出:1、2、3、4
。但如果介入协程技术那么就可以实现函数见代码切换执行,最终输入:1、3、2、4
。
在Python中有多种方式可以实现协程,例如:
- greenlet,是一个第三方模块,用于实现协程代码(Gevent协程就是基于greenlet实现)
- yield,生成器,借助生成器的特点也可以实现协程代码。
- asyncio,在Python3.4中引入的模块用于编写协程代码。
- async & awiat,在Python3.5中引入的两个关键字,结合asyncio模块可以更方便的编写协程代码。
协程的意义
计算型的操作,利用协程来回切换执行,没有任何意义,来回切换并保存状态 反倒会降低性能。
IO型的操作,利用协程在IO等待时间就去切换执行其他任务,当IO操作结束后再自动回调,那么就会大大节省资源并提供性能,从而实现异步编程(不等待任务结束就可以去执行其他代码)。
基于yield
import time
"""
使用yield简单实现协程
"""
def work1():
counter = 0
while True:
print("work1....执行中", counter)
time.sleep(0.5)
yield
counter += 1
def work2():
counter = 0
while True:
print("work2.....................执行中", counter)
time.sleep(0.5)
yield
counter += 1
if __name__ == '__main__':
gen1 = work1()
gen2 = work2()
while True:
next(gen1)
next(gen2)
基于greenlet
pip install greenlet
from greenlet import greenlet
import time
def work1():
while True:
print("work1....")
# 切换其他协程
gl2.switch()
time.sleep(0.5)
def work2():
while True:
print("work2...............")
# 切换其他协程
gl1.switch()
time.sleep(0.5)
if __name__ == '__main__':
# 创建两个greenlet对象
gl1 = greenlet(work1)
gl2 = greenlet(work2)
gl1.switch()
基于gevent
pip install gevent
- 使用gevent.sleep(10)
import gevent
def work1():
for i in range(5):
print("work1......", i)
gevent.sleep(1)
def work2():
for i in range(5):
print("work2.............", i)
gevent.sleep(1)
if __name__ == '__main__':
# 指派任务
g1 = gevent.spawn(work1)
g2 = gevent.spawn(work2)
gevent.sleep(10)
# time.sleep(10)
# g1.join()
# g2.join()
并发下载器
from gevent import monkey
monkey.patch_all()
from urllib import request
import gevent
def download_url(img_url, file_name):
print("开始下载:",img_url, file_name)
# 下载数据
response = request.urlopen(img_url)
# 准备文件,接收网络数据
with open(file_name, "wb") as file:
while True:
data = response.read(4096)
if data:
file.write(data)
else:
break
print("\33[42;1m 文件下载完毕 \033[0m{},文件名:{}".format(img_url, file_name))
if __name__ == '__main__':
img_url1 = "http://img.mp.itc.cn/upload/20170716/8e1b835f198242caa85034f6391bc27f.jpg"
img_url2 = "http://img.mp.sohu.com/upload/20170529/d988a3d940ce40fa98ebb7fd9d822fe2.png"
img_url3 = "http://image.uczzd.cn/11867042470350090334.gif?id=0&from=export"
g1 = gevent.spawn(download_url, img_url1, "1.gif")
g2 = gevent.spawn(download_url, img_url2, "2.gif")
g3 = gevent.spawn(download_url, img_url3, "3.gif")
gevent.joinall([g1, g2, g3])
# g1.join()
# g2.join()
# g3.join()
基于asyncio
在Python3.4之前官方未提供协程的类库,一般大家都是使用greenlet等其他来实现。在Python3.4发布后官方正式支持协程,即:asyncio模块。
import asyncio
@asyncio.coroutine
def func1():
print(1)
yield from asyncio.sleep(2) # 遇到IO耗时操作,自动化切换到tasks中的其他任务
print(2)
@asyncio.coroutine
def func2():
print(3)
yield from asyncio.sleep(2) # 遇到IO耗时操作,自动化切换到tasks中的其他任务
print(4)
tasks = [
asyncio.ensure_future( func1() ),
asyncio.ensure_future( func2() )
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
注意:基于asyncio模块实现的协程比之前的要更厉害,因为他的内部还集成了遇到IO耗时操作自动切花的功能。
基于async & awit
async & awit 关键字在Python3.5版本中正式引入,基于他编写的协程代码其实就是 上一示例 的加强版,让代码可以更加简便。
Python3.8之后 @asyncio.coroutine
装饰器就会被移除,推荐使用async & awit
关键字实现协程代码。
import asyncio
async def func1():
print(1)
await asyncio.sleep(2)
print(2)
async def func2():
print(3)
await 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))
案例
同步编程实现
"""
下载图片使用第三方模块requests,请提前安装:pip3 install requests
"""
import requests
def download_image(url):
print("开始下载:",url)
# 发送网络请求,下载图片
response = requests.get(url)
print("下载完成")
# 图片保存到本地文件
file_name = url.rsplit('_')[-1]
with open(file_name, mode='wb') as file_object:
file_object.write(response.content)
if __name__ == '__main__':
url_list = [
'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
'https://www2.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg',
'https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg'
]
for item in url_list:
download_image(item)
基于协程的异步编程实现
"""
下载图片使用第三方模块aiohttp,请提前安装:pip3 install aiohttp
"""
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import aiohttp
import asyncio
async def fetch(session, url):
print("发送请求:", url)
async with session.get(url, verify_ssl=False) as response:
content = await response.content.read()
file_name = url.rsplit('_')[-1]
with open(file_name, mode='wb') as file_object:
file_object.write(content)
async def main():
async with aiohttp.ClientSession() as session:
url_list = [
'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
'https://www2.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg',
'https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg'
]
tasks = [asyncio.create_task(fetch(session, url)) for url in url_list]
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())
基于协程的异步编程 要比 同步编程的效率高了很多。因为:
- 同步编程,按照顺序逐一排队执行,如果图片下载时间为2分钟,那么全部执行完则需要6分钟。
- 异步编程,几乎同时发出了3个下载任务的请求(遇到IO请求自动切换去发送其他任务请求),如果图片下载时间为2分钟,那么全部执行完毕也大概需要2分钟左右就可以了。
小结
关于协程有多种实现方式,目前主流使用是Python官方推荐的asyncio模块和async&await关键字的方式,例如:在tonado、sanic、fastapi、django3 中均已支持。
协程一般应用在有IO操作的程序中,因为协程可以利用IO等待的时间去执行一些其他的代码,从而提升代码执行效率。