协程,也称用户级线程,在不开辟线程的基础上完成任务,也就是在单线程的情况下完成多任务,多个任务按照一定顺序交替执行,通俗理解只要在def里面看到只有一个yield关键字表示的就是协程。
学习协程的目的:不开辟线程完成多任务,为了更加节省内存资源。
原始协程程序
import time
# 定义协程1
def work1():
for i in range(5):
print("work1执行中...")
time.sleep(0.2)
yield
# 定义协程2
def work2():
for i in range(5):
print("work2执行中...")
time.sleep(0.2)
yield # 本质上使用yield就是一个生成器
if __name__ == '__main__':
# 创建协程
g1 = work1()
g2 = work2()
print(g1, g2)
for i in range(5):
# 使用next函数启动协程
next(g1)
next(g2)
#g1 g2 有序交替执行
中级协程
为了更好的使用协程完成多任务,python中的greenlet模块对其进行封装,使切换协程更加容易。
greentlet封装的是yield,可以更加清楚的切换到某个协程代码。
pip install greenlet
import greenlet
import greenlet
import time
# 任务1
def work1():
for i in range(5):
print("work1执行中...")
time.sleep(0.2)
# 切换到第二个协程执行对应的任务
g2.switch()
# 任务2
def work2():
for i in range(5):
print("work2执行中...")
time.sleep(0.2)
# 切换到第一个协程执行对应的任务
g1.switch()
# 创建协程1
g1 = greenlet.greenlet(work1)
# 创建协程2
g2 = greenlet.greenlet(work2)
if __name__ == '__main__':
# 切换到第一个协程执行对应的任务
g1.switch()
高级协程,使用模块gevent
greenlet已经封装了yield,但是这个还要人工切换,gevet是一个比greenlet更强大而且能够自动切换任务的第三方库。
gevent内部封装了greenlet,其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如文件操作,time.sleep, 网络请求, tcp accept, tcp recv等等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
pip install gevent
import gevent
gevent切换执行协程
import gevent
def work(n):
for i in range(n):
# 获取当前协程
print(gevent.getcurrent(), i)
#用来模拟一个耗时操作,注意不是time模块中的sleep
gevent.sleep(1)
g1 = gevent.spawn(work, 5)
g2 = gevent.spawn(work, 5)
g3 = gevent.spawn(work, 5)
g1.join()
g2.join()
g3.join()
<Greenlet "Greenlet-0" at 0x2838bd40e48: work(2)> 0
<Greenlet "Greenlet-1" at 0x2838bec5148: work(2)> 0
<Greenlet "Greenlet-2" at 0x2838bec5348: work(2)> 0
<Greenlet "Greenlet-0" at 0x2838bd40e48: work(2)> 1
<Greenlet "Greenlet-1" at 0x2838bec5148: work(2)> 1
<Greenlet "Greenlet-2" at 0x2838bec5348: work(2)> 1
gevent打猴子补丁切换执行协程
form gevent import monkey
monkey.patch_all()
import gevent
import time
from gevent import monkey
# 打补丁
monkey.patch_all()
def task1():
for i in range(5):
print("任务1执行中...")
time.sleep(0.2)
# gevent.sleep(0.2)
def task2():
for i in range(5):
print("任务2执行中...")
time.sleep(0.2)
# gevent.sleep(0.2)
if __name__ == '__main__':
g1 = gevent.spawn(task1)
g2 = gevent.spawn(task2)
# 主线程等待协程执行完成以后程序再退出
# g1.join()
# g2.join()
# 注意点: 让协程进行工作需要保证线程不退出
while True:
time.sleep(0.2) # tcp_server.accept()
提示: 不需要加join的前提, 1. 线程需要一直运行 2. 在线程里面需要捕获到耗时操作可以自动切换到其它协程执行对应的任务
demo 使用gevent多任务下载图片
import urllib.request
import gevent
from gevent import monkey
monkey.patch_all()
# 下载图片的任务
def download_img(img_url, img_name):
try:
# 获取当前协程
print("download_img:", gevent.getcurrent())
# 根据图片地址打开网络资源文件
file = urllib.request.urlopen(img_url)
# 打开下载的文件,写入网络文件数据
with open(img_name, "wb") as img_file:
while True:
# 获取网络资源文件数据
img_data = file.read(1024)
if img_data:
# 把网络资源文件的数据写入到指定下载文件里面
img_file.write(img_data)
else:
# 代码执行到此,说明文网络中的文件数据读取完成
break
except Exception as e:
print("网络异常,下载失败:", e)
else:
print("下载成功:", img_name)
if __name__ == '__main__':
# 准备下载图片的地址
img_url1 = "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1293860636,1088191402&fm=27&gp=0.jpg"
img_url2 = "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2086972466,4153089489&fm=27&gp=0.jpg"
img_url3 = "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=4115825325,3478781251&fm=27&gp=0.jpg"
# 创建协程 参数不是元组的形式,而是位置参数
g1 = gevent.spawn(download_img, img_url1, "1.jpg")
g2 = gevent.spawn(download_img, img_url2, "2.jpg")
g3 = gevent.spawn(download_img, img_url3, "3.jpg")
print(g1, g2, g3)
# 主线程等待所有的协程执行完成以后程序再退出
gevent.joinall([g1, g2, g3])
进程、线程、线程的对比
- 进程是操作系统资源分配的基本单位
- 线程是CPU调度的基本单位
- 进程切换需要的资源最大,效率很低
- 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
- 协程切换任务资源很小,效率高