python:并发编程之协程

python:协程

一、生成器回顾

1、什么是生成器

​ 在上一篇文章中,我们讲到了python中的强大工具——生成器,那么所谓生成器就是特殊的迭代器,并且生成器中包含了一定的算法逻辑,你需要什么数据,我们就使用生成器生成一些有规律的数据,这样就解决了多余数据占用内存资源的问题

2、如何创建生成器

方法一:
# 就是将我们的列表推导式中的[] 改为 (),那么一个简单的生成器就制作完成了,并且可以使用for遍历来
# 生成1-5的整数
my_generator = (i for i in range(1,6))
# 并且该生成器和列表推导式的输出是完全不同的
# 列表推导式输出是一个列表
# 而生成器输出的是一个generator生成器对象
方法二:
# 使用一个函数和 yield 关键字配合,创建一个生成器对象,那么只要一个函数中有yield关键字,那么他将不再是函数,而是一个生成器
# 比如说使用生成器生成斐波那契数列
def func(n):
    i, a, b = 1, 0, 1
    while i <= n:
        a, b = b, a + b
        yield a
        i += 1
    else:
        return f"生成器超出生成范围"


if __name__ == '__main__':
    generator = func(6)
    # 正常迭代出生成器生成的数据
    for item in generator:
        print(item)

    # 无限迭代生成的数据,当超出范围打印终止迭代的异常数据
    try:
        while True:
            print(next(generator))
    except StopIteration as s:
        print(s.value)

3、那么为什么要在学习协程之前带领大家回顾生成器呢?

​ 因为生成器和我们目前学习的多任务,并发的第三种方式——协程,有着一定的渊源,所以在学习协程之前,带领大家使用生成器来模拟一个协程并发的效果,这样对我们学习协程的时候有很大的帮助,那么使用生成器模拟并发,最重要的是yield关键字,那么来看一下吧

4、生成器模拟并发

import time

# 上传生成器
def upload():
    while True:
        # 延迟0.5秒
        time.sleep(0.5)
        print("开始上传")
        # yield后不写默认返回None
        yield

# 下载生成器
def download():
    while True:
        # 延迟0.5秒
        time.sleep(0.5)
        print("开始下载")
        # yield后不写默认返回None
        yield


if __name__ == '__main__':
    up = upload()
    down = download()
    # 计算开始时间
    start = time.time()
    # 连续唤醒
    for _ in range(5):
        # 一次性唤醒
        next(up)
        down.send(None)
    # 计算结束时间
    end = time.time()
    # 打印所用时间
    print(f"所用时间:{'{:.0f}'.format(end-start)}秒")

总结:

​ 这个过程实际上就是先让up运行,再让down运行,采用的是一次性唤醒的方式,让两个生成器同步执行,也就协调步调,挨个执行。为什么呢?因为每一次唤醒,到yield关键字就停止了(因为yield关键字会记住执行的位置,下一次再从这个地方执行),然后又开始执行下一个生成器,这样轮流交替执行,同样实现了我们的并发,并且这种并发的运行效率是极高的。

​ 那么接下来我们学习实现多任务的第三种方式——协程

二、协程

1、什么是协程

​ 协程:又叫微线程,或者纤程,是实现多任务的第三种方式,它是单线程下的并发,也就是说它在一个线程下并发,但是可以在多个不同的线程中并发。协程是比线程更小的执行单元,或者说占用的执行单位资源比线程更小,因为它自带CPU的上下文,这样,只要在合适的时机,我们可以把一个协程切换到另一个协程,只要这个过程中保存或恢复CPU上下文,那么程序还是可以运行,而协程就具备这样的能力。

理解:

​ 在一个协程中的某个函数中,我们可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行(我们这里并不是使用函数调用的方式),并且切换的次数以及什么时候再切换到原来的函数,都由开发者自己确定。

2、什么是CPU上下文

(1)并发:

​ 我们都知道 ,Linux 是一个多任务的操作系统,它支持远大于CPU数量的任务同时运行。当然这些任务实际上并不是真正的在同时运行,而是系统在很短的时间内,将CPU的资源轮流分配给他们,这个速度是非常快的,所以造成的多任务同时运行的假象,那么这就是我们这几天一直学习的话题——并发

(2)寄存器和程序计数器的功能

​ 而在每个任务运行前,CPU需要直到这些任务从哪里加载,又从哪里开始运行,也就是说CPU它不知道,那么就需要系统事先帮CPU设置好 寄存器和程序计数器

​ 寄存器,是CPU内置的容量小,但是运行速度极快的内存。而程序计数器,则是用来存储 CPU 正在执行的指令位置,或者即将执行的下一条指令位置。他们两个都是在 CPU 运行任何任务前,必须的依赖环境,因此也叫做 CPU 上下文

(3)理解

​ 总的来说,就是 CPU 在运行任务前,都要依赖程序计数器来找到上一次运行终止的位置,或者下一次将要运行的位置,而 寄存器则是 CPU 快速运行任务的 依赖环境。换成白话文也就是说:CPU运行任务到哪里,我都知道位置,和下一次应该运行的位置,这就是CPU上下文。

3、协程与线程的差异

​ 在实现多任务时,线程切换从系统层面来看,远不止保存和恢复CPU上下文这么简单,操作系统为了程序运行的高效性,每个线程都自己缓存Cache(计算中的高速缓存,是一种数据存储技术)等数据,操作系统还会帮我们做这些数据的恢复操作,所以线程的切换非常损耗性能。但是协程的切换单纯的操作CPU的上下文,也就是说切换CPU去不同的任务中从刚才停止的代码开始执行,所以一秒钟切换个上百万次是完全没有问题的。

​ 那么咱们刚刚回顾我们的生成器的时候,使用yield关键字来回的切换两个生成器,实际上很容易就实现了我们协程的功能。

4、如何实现协程

(1)greenlet模块

步骤一:

#导入greenlet模块下的greenlet类 
from greenlet import greenlet
# greenlet 译为 绿色小鸟

步骤二:

# 实例化协程对象
# 对象名 = greenlet(目标函数名)

步骤三:

# switch译为:开关,所以就是打开协程
# 对象名.switch(args, kwargs)
# args:以元组的方式给目标函数传参
# kwargs :以元组的方式给目标函数传参

实例:

from greenlet import greenlet
import time


def uploda():
    while True:
        time.sleep(0.5)
        print("正在上传...")
        # 打开download线程
        g2.switch()

def download():
    while True:
        time.sleep(0.5)
        print("正在下载")
        # 打开upload线程
        g1.switch()


if __name__ == '__main__':
    g1 = greenlet(uploda)
    g2 = greenlet(download )
    # 打开upload线程
    g1.switch()

总结:

​ 在这里我们发现了一个问题,在使用 greenlet 实现协程的时候,如果遇到了大量的 IO(输入输出、网络访问、文件下载)等耗时操作的时候,那么就一直处在一个协程中,也就是发生了堵塞,只有当耗时操作执行完了才会切换到下一个协程,并且还要在每个协程完毕后都要开启下一个协程,所以说这对客户而言是很不理想的,并且也影响了我们的开发效率。

​ 也就是说 greenlet 只是 提供了一种比 generator(生成器)更加便捷的一种方式,但当切换到一个任务执行时如果遇到了IO耗时操作,那就原地堵塞,并没有解决遇到IO自动切换来提升效率的问题。

(2)gevent 模块

​ python 中还要一个比 greenlet 模块更强大,并且能够在遇到 IO耗时操作时 ,自动切换任务的模块——gevent ,其原理是:当一个 gevent 遇到 IO耗时操作时,比如访问网络,它就自动切换到其他 gevent 上,当等到 IO 操作完成时,在适当的时候切换回来继续执行。

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

步骤一:

​ 因为 gevent 是一个第三方库,所以 pycharm 中没有自带,那么我们第一步操作就是在 Ubuntu 系统下安装 gevent ,其安装方式是我们众所周知的在线安装,如下:

sudo apt-get install python3-gevent

步骤二:

# 导入该模块,gevent在有道翻译中译为:并行开发
import gevent

步骤三:

# 实例化gevent对象,spawn译为:产卵
对象名 = gevent.spawn(cls, args, kwargs)
# cls:表示目标函数
# args:以元组方式给目标函数传参
# kwargs:以字典方式给目标函数传参

步骤四:

# 耗时操作
gevent.time(秒数)
# 注意:这里不能使用time模块下的耗时操作,即time.sleep(),因为gevent无法识别
# 所以只能使用gevent可以识别的耗时操作

步骤三:

# 开启协程,并等待协程对象结束
协程对象.join()
# 有一种方法,不用实例化对象,直接开启join,或者说是连写的方式
# 注意:是列表的形式
gevent.joinall([
    gevent.spawn(目标函数名称),
    gevent.spawn(目标函数名称)])

实例:

import gevent
import time

def upload():
    print("开始上传...")
    gevent.sleep(1)
    print("上传成功")

def download():
    print("开始下载...")
    gevent.sleep(1)
    print("下载成功")

if __name__ == '__main__':
    up = gevent.spawn(upload)
    down = gevent.spawn(download)
    print(time.time())
    up.join()
    down.join()
    print(time.time())

5、monkey补丁

​ 如果说在项目开发中,你的协程中没有使用 gevent 可识别的耗时操作,而是使用了time等其他模块的耗时操作,那么该怎么办?总不肯去一个个找?重新修改?那绝对不可能,太浪费时间了。

第一步:
# 导入gevent模块中的一个mokey
from gevent import monkey
第二步:
# 在所有程序运行之前,打上一个补丁,patch译为:缝补
monkey.patch_all()
# 这行代码是专门针对于协程下不能识别的耗时操作,它会自动将这些不能识别的耗时操作,替换为它可以识别的耗时操作
实例:
# 导入gevent模块下的猴子
from gevent import monkey
import gevent
import time

def upload():
    print("开始上传...")
    time.sleep(1)
    print("上传成功")

def download():
    print("开始下载...")
    time.sleep(1)
    print("下载成功")

if __name__ == '__main__':
    # 在所有程序运行之前使用猴子给所有协程下的耗时操作打补丁
    monkey.patch_all()
    print(time.time())
    gevent.joinall([
        gevent.spawn(upload),
        gevent.spawn(download)
    ])
    print(time.time())

6、总结协程的特点

(1)必须在一个单线程里实现并发,但是可以在不同的多个线程中实现并发

(2)修改全局变量不要用锁

(3)自动保存CPU上下文

(4)所有遇到 IO 操作就会切换到其他协程,在合适的时间再切换回来

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

御弟謌謌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值