协程Python 中实现多任务耗资源最小的方式


Posted in Python onOctober 19, 2020

协程,又称微线程,纤程。英文名 Coroutine。

协程是 Python 中另外一种实现多任务的方式,只不过比线程更小,占用更小执行单元(理解为需要的资源)。

为啥说它是一个执行单元,因为它自带 CPU 上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。

通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定。

协程和线程差异

在实现多任务时, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。

操作系统为了程序运行的高效性每个线程都有自己缓存 Cache 等等数据,操作系统还会帮你做这些数据的恢复操作,所以线程的切换非常耗性能。

但是协程的切换只是单纯的操作 CPU 的上下文,所以一秒钟切换个上百万次系统都抗得住。

之前我们讲过 yield 关键字,现在就用它来实现多任务。

例子:

import time

def task_1():
  while True:
    print("--1--")
    time.sleep(0.5)
    yield

def task_2():
  while True:
    print("--2--")
    time.sleep(0.5)
    yield

def main():
  t1 = task_1()
  t2 = task_2()
  while True:
    next(t1)
    next(t2)

if __name__ == "__main__":
  main()

运行过程:

先让 t1 运行一会,当 t1 遇到 yield 的时候,再返回到 main() 循环的地方,然后执行 t2 , 当它遇到 yield 的时候,再次切换到 t1 中,这样 t1 和 t2 就交替运行,最终实现了多任务,协程。

运行结果:

协程Python 中实现多任务耗资源最小的方式

greenlet

为了更好使用协程来完成多任务,Python 中的 greenlet 模块对其封装,从而使得切换任务变的更加简单。

首先你要安装一下 greenlet 模块。

pip3 install greenlet
from greenlet import greenlet
import time

def test1():
  while True:
    print("---A--")
    gr2.switch()
    time.sleep(0.5)

def test2():
  while True:
    print("---B--")
    gr1.switch()
    time.sleep(0.5)

gr1 = greenlet(test1)
gr2 = greenlet(test2)

# 切换到gr1中运行
gr1.switch()

运行结果:

协程Python 中实现多任务耗资源最小的方式

和我们之前用 yield 实现的效果基本一样,greenlet 其实是对 yield 进行了简单的封装。

greenlet 实现多任务要比 yield 更简单,但是我们以后还是不用它。

上面例子中的延时是0.5秒,如果延迟是100秒,那么程序就会卡住100秒,就算有其他需要执行的任务,系统也不会切换过去,这100秒的时间是无法利用的。

这个问题下面来解决。

gevent

greenlet 已经实现了协程,但是还是得进行人工切换,是不是觉得太麻烦了。

Python 还有一个比 greenlet 更强大的并且能够自动切换任务的模块 gevent。

gevent 是对 greenlet 的再次封装。

其原理是当一个 greenlet 遇到 IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的 greenlet,等到 IO 操作完成,再在适当的时候切换回来继续执行。

由于 IO 操作非常耗时,经常使程序处于等待状态,有了gevent 为我们自动切换协程,就保证总有 greenlet 在运行,而不是等待 IO。

首先还是得先安装 gevent。

pip3 install gevent

例子:

import gevent

def f(n):
  for i in range(n):
    print(gevent.getcurrent(), i)

g1 = gevent.spawn(f, 3)
g2 = gevent.spawn(f, 3)
g3 = gevent.spawn(f, 3)
g1.join()
g2.join()
g3.join()

运行结果:

<Greenlet at 0x35aae40: f(3)> 0
<Greenlet at 0x35aae40: f(3)> 1
<Greenlet at 0x35aae40: f(3)> 2
<Greenlet at 0x374a780: f(3)> 0
<Greenlet at 0x374a780: f(3)> 1
<Greenlet at 0x374a780: f(3)> 2
<Greenlet at 0x374a810: f(3)> 0
<Greenlet at 0x374a810: f(3)> 1
<Greenlet at 0x374a810: f(3)> 2

可以看到,3个 greenlet 是依次运行而不是交替运行。

这还无法判断 gevent 是否实现了多任务的效果,最好的判断情况是在运行结果中 0 1 2 不按顺序出现。

在 gevent 的概念中,我们提到 gevent 在遇到延时的时候会自动切换任务。

那么,我们先给上面的例子添加延时,再看效果。

import gevent
import time

def f(n):
  for i in range(n):
    print(gevent.getcurrent(), i)
    time.sleep(0.5)

g1 = gevent.spawn(f, 3)
g2 = gevent.spawn(f, 3)
g3 = gevent.spawn(f, 3)
g1.join()
g2.join()
g3.join()

运行结果:

<Greenlet at 0x36aae40: f(3)> 0
<Greenlet at 0x36aae40: f(3)> 1
<Greenlet at 0x36aae40: f(3)> 2
<Greenlet at 0x384a780: f(3)> 0
<Greenlet at 0x384a780: f(3)> 1
<Greenlet at 0x384a780: f(3)> 2
<Greenlet at 0x384a810: f(3)> 0
<Greenlet at 0x384a810: f(3)> 1
<Greenlet at 0x384a810: f(3)> 2

在添加了延时之后,运行结果并没有改变。

其实,gevent 要的不是 time.sleep() 的延时,而是 gevent.sleep() 的延时。

import gevent

def f(n):
  for i in range(n):
    print(gevent.getcurrent(), i)
    gevent.sleep(0.5)

g1 = gevent.spawn(f, 3)
g2 = gevent.spawn(f, 3)
g3 = gevent.spawn(f, 3)
g1.join()
g2.join()
g3.join()

join 还有一种更简单的写法。

import time
import gevent

def f(n):
  for i in range(n):
    print(gevent.getcurrent(), i)
    gevent.sleep(0.5)

gevent.joinall([
  gevent.spawn(f, 3),
  gevent.spawn(f, 3),
  gevent.spawn(f, 3)
])

一般都是后面的这种写法。

运行结果:

<Greenlet at 0x2e5ae40: f(3)> 0
<Greenlet at 0x2ffa780: f(3)> 0
<Greenlet at 0x2ffa810: f(3)> 0
<Greenlet at 0x2e5ae40: f(3)> 1
<Greenlet at 0x2ffa780: f(3)> 1
<Greenlet at 0x2ffa810: f(3)> 1
<Greenlet at 0x2e5ae40: f(3)> 2
<Greenlet at 0x2ffa780: f(3)> 2
<Greenlet at 0x2ffa810: f(3)> 2

这下终于实现多任务的效果了, gevent 在遇到延时的时候,就自动切换到其他任务。

这里是将 time 中的 sleep 换成了 gevent 中的 sleep。

那如果有网络程序,网络程序中也有许多堵塞,比如 connect, recv,accept,需要不需要换成 gevent 中的对应方法。

理论上来说,是要换的。如果想用 gevent,那么就要把所有的延时操作,堵塞这一类的函数,统统换成 gevent 中的对应方法。

那有个问题,万一我的代码已经写了10万行了,这换起来怎么破......

有什么办法不需要手动修改么,有,打个补丁即可。

import time
import gevent
from gevent import monkey

# 有耗时操作时需要
# 将程序中用到的耗时操作的代码,换为gevent中自己实现的模块
monkey.patch_all() 

def f(n):
  for i in range(n):
    print(gevent.getcurrent(), i)
    time.sleep(0.5)

g1 = gevent.spawn(f, 3)
g2 = gevent.spawn(f, 3)
g3 = gevent.spawn(f, 3)
g1.join()
g2.join()
g3.join()

monkey.patch_all() 会自动去检查代码,将所有会产生延时堵塞的方法,都自动换成 gevent 中的方法。

运行结果:

<Greenlet at 0x3dd91e0: f(3)> 0
<Greenlet at 0x3dd9810: f(3)> 0
<Greenlet at 0x3dd99c0: f(3)> 0
<Greenlet at 0x3dd91e0: f(3)> 1
<Greenlet at 0x3dd9810: f(3)> 1
<Greenlet at 0x3dd99c0: f(3)> 1
<Greenlet at 0x3dd91e0: f(3)> 2
<Greenlet at 0x3dd9810: f(3)> 2
<Greenlet at 0x3dd99c0: f(3)> 2

总结:

通过利用延时的时间去做其他任务,把时间都利用起来,这就是协程最大的意义。

到此这篇关于协程Python 中实现多任务耗资源最小的方式的文章就介绍到这了,更多相关Python多任务耗资源最小方式内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
python和shell变量互相传递的几种方法
Nov 20 Python
Python写的一个简单DNS服务器实例
Jun 04 Python
初步剖析C语言编程中的结构体
Jan 16 Python
python 捕获 shell/bash 脚本的输出结果实例
Jan 04 Python
R vs. Python 数据分析中谁与争锋?
Oct 18 Python
Python实现多进程共享数据的方法分析
Dec 04 Python
Python 删除整个文本中的空格,并实现按行显示
Jul 24 Python
python支付宝支付示例详解
Aug 22 Python
Python 多线程其他属性以及继承Thread类详解
Aug 28 Python
python字符串的拼接方法总结
Nov 18 Python
Django DRF路由与扩展功能的实现
Jun 03 Python
Python实现自动签到脚本的示例代码
Aug 19 Python
python爬取音频下载的示例代码
Oct 19 #Python
Python爬虫教程知识点总结
Oct 19 #Python
自定义Django_rest_framework_jwt登陆错误返回的解决
Oct 18 #Python
如何利用python读取micaps文件详解
Oct 18 #Python
Python中Yield的基本用法
Oct 18 #Python
Anaconda+spyder+pycharm的pytorch配置详解(GPU)
Oct 18 #Python
Python通过format函数格式化显示值
Oct 17 #Python
You might like
php防注
2007/01/15 PHP
两个开源的Php输出Excel文件类
2010/02/08 PHP
php匹配字符中链接地址的方法
2014/12/22 PHP
在win系统安装配置 Memcached for PHP 5.3 图文教程
2015/03/03 PHP
分享精心挑选的23款美轮美奂的jQuery 图片特效插件
2012/08/14 Javascript
jQuery隔行变色与普通JS写法的对比
2013/04/21 Javascript
JavaScript中Number.MAX_VALUE属性的使用方法
2015/06/04 Javascript
jQuery实现仿腾讯视频列表分页效果的方法
2015/08/07 Javascript
js判断某个字符出现的次数的简单实例
2016/06/03 Javascript
IE8兼容Jquery.validate.js的问题
2016/12/01 Javascript
微信公众号开发 自定义菜单跳转页面并获取用户信息实例详解
2016/12/08 Javascript
基于BootstrapValidator的Form表单验证(24)
2016/12/12 Javascript
vue2滚动条加载更多数据实现代码
2017/01/10 Javascript
详解关于vue2.0工程发布上线操作步骤
2018/09/27 Javascript
react-router 路由切换动画的实现示例
2018/12/03 Javascript
基于javascript实现贪吃蛇经典小游戏
2020/04/10 Javascript
vue双击事件2.0事件监听(点击-双击-鼠标事件)和事件修饰符操作
2020/07/27 Javascript
[04:20]DOTA2-DPC中国联赛 正赛 VG vs LBZS 选手采访 1月19日
2021/03/11 DOTA
谈谈Python中的while循环语句
2019/03/10 Python
python matplotlib库直方图绘制详解
2019/08/10 Python
python读取csv文件指定行的2种方法详解
2020/02/13 Python
Groupon法国官方网站:特卖和网上购物高达-70%
2019/09/02 全球购物
荣耀俄罗斯官网:HONOR俄罗斯
2020/10/31 全球购物
Clos19英国:高档香槟、葡萄酒和烈酒在线购物平台
2020/07/10 全球购物
教学改革实施方案
2014/03/31 职场文书
体育课课后反思
2014/04/24 职场文书
项目投资建议书
2014/05/16 职场文书
行政监察建议书
2014/05/19 职场文书
学校元旦晚会开场白
2014/12/14 职场文书
大学宣传委员竞选稿
2015/11/19 职场文书
2019年大学生职业生涯规划书
2019/03/25 职场文书
简历自我评价:教师师德表现自我评价
2019/04/24 职场文书
redis限流的实际应用
2021/04/24 Redis
python实现层次聚类的方法
2021/11/01 Python
MongoDB支持的索引类型
2022/04/11 MongoDB
windows server2008 开启端口的实现方法
2022/06/25 Servers