Python学习笔记 协程

目录
  ~ 什么是协程?
  ~ 单线程能实现并发吗?
  ~ 思考:上面的单线程并发(协程)有意义吗?
  ~ 如何自动检测 IO 实现有意义的协程?
  ~ 简单介绍一下 Greenlet 类
  ~ 协程的优点和缺点
  ~ Coroutines and Tasks


什么是协程?

  答:单线程实现并发,每一个并发的任务就是一个协程。协程又称微线程,纤程,它的关键之处在于自动监测 IO 然后在协程间进行切换。它存在的目的是为了增加程序的执行效率。一个进程至少有一个线程,而一个线程至少有一个协程。


单线程能实现并发吗?

  答:那首先得明确一下什么是并发。并发是指多个任务轮流使用资源,并发的关键核心是任务之间的切换状态的保存。那么可以认为,能实现多任务切换并保存每个任务的执行状态,那么就是能够实现并发。

    现在回到原问题,单线程能够实现并发吗?答案是肯定的!(不然我写这篇博客干嘛)Python 中有一个关键字能自动保存函数的执行状态——yield。函数中使用 yield 语句从函数返回,并保存它的执行状态。然后可以利用 next() 或 send() 函数重新启动函数(send() 方法还可以往函数内传递参数)。实际上这就实现了不同任务的切换,在主流程和任务函数之间的切换,即实现了并发。


给个代码看看

  实际上这与生成器的概念一样,关于生成器可参阅:Python学习笔记 生成式和生成器

def work():
    print('切换至另一任务执行 1')
    yield
    print('切换至另一任务执行 2')
    yield
    print('另一任务结束')


def main():
    task = work()
    print('一个任务开始')
    next(task)
    print('一个任务执行中')
    next(task)
    print('一个任务继续执行')
    try:
        next(task)
    except StopIteration:
        # 生成器对象结束时产生的异常,放在 for 中会自动处理
        # 这里把它忽略掉
        pass
    print('一个任务结束')


if __name__ == '__main__':
    main()

  从上面的代码执行结果可以看到,两个任务你一句我一句的输出,实现了多任务的切换,即并发。


思考:上面的单线程并发(协程)有意义吗?

  参考多线程实现并发,在单线程实现并发(即协程)的目的是为了提高程序的执行效率。多线程时,发生线程切换的原因有两种,第一种是一个线程遇到 IO 时切换至另一线程。这种切换避免了 CPU 在一个线程 IO 时的无意义等待,提高了 CPU 的利用率,所以能够提高程序的执行效率;第二种切换是线程的执行时间达到阈值系统进行强制切换。这种切换只能使 CPU 做到对各个线程雨露均沾,而不能提高程序的执行效率。对于计算密集型的任务,过多的无意义切换反而会降低执行效率。

  简单来说,IO 时切换才有意义。那么 yield 实现的协程有意义吗?答案是否定的。yield 不能检测 IO,遇到 IO 时,yield 会等待 IO 而不是切换至其他协程。下面给个示例感受一下:

import time


def work():
    print('切换至另一任务执行 1')
    time.sleep(100)
    # 利用 time 模块模拟 IO
    yield


def main():
    task = work()
    print('一个任务开始')
    try:
        next(task)
    except StopIteration:
        # 生成器对象结束时产生的异常,放在 for 中会自动处理
        # 这里把它忽略掉
        pass
    print('任务结束')


if __name__ == '__main__':
    main()

  你会发现“一个任务开始后”,你要等100多秒才回到看“任务结束”。


如何自动检测 IO 实现有意义的协程?

  答:需要用到 gevent 模块。该模块不是内置的,需要安装。安装方法是打开 cmd ,然后输入:pip3 install greenlet,最后回车即可。如下图:

在这里插入图片描述
  gevent 模块能帮我们自动检测 IO 并切换至另一个协程。协程与线程和进程的模式相似:线程模块里有 Thread 类,用于创建线程对象;进程模块里有 Process 类,用于创建进程对象;而 gevent 模块中有 Greenlet 类,用于创建协程对象。进程、线程和协程都有 start() 和 join() 方法,且功能类似。


简单介绍一下 Greenlet 类

  Greenlet 是 gevent 模块提供的一个类,它是 greenlet 的子类,用于构造一个轻量级的任务协作执行单元。greenlet 类是定义在 greenlet 模块中的一个类,利用 greenlet 对象可以实现与 yield 一样的功能,即多任务切换,而且 greenlet 对象比 yield 用起来更简单方便。但 greenlet 对象也不能自动检测 IO。Greenlet 在 greenlet 的基础上实现了对 IO 的自动检测。

Greenlet 构造函数

    class gevent.Greenlet(run=None, *args, **kwargs)
        功能:创建一个新的 Greenlet 对象,并将其安排为运行 run(*args, **kwargs)
        
        参数:run 是一个可调用对象,表示协程要执行的任务
             *args 表示 run 的位置参数
             **kwargs 表示 run 的关键字参数
             
        返回值:一个新的 Greenlet 对象

Greenlet 属性

		minimal_ident
            功能:获取协程的唯一标识编号,该编号在协程死亡后会被重用,类似于 Thread.ident
            返回值:一个小的整数
            说明:这是一个静态属性

        name:是一个字符串,表示协程的名字,默认使用 minimal_ident 构造一个名字,可修改

		value:表示 run 的返回值,与 get() 方法效果一样

Greenlet 对象常用方法及解析

		start()
            功能:安排 greenlet 在此循环迭代中运行
            参数:无
            返回值:None
            说明:并不是立即开始执行,而是加到循环迭代中,相当于是获取了切换的资格

        join(timeout=None)
            功能:阻塞至协程结束或超时
            参数:timout 表示阻塞的秒数,默认无限制
            返回值:始终为 None
            
        ready()
            功能:判断协程是否执行完毕
            参数:无
            返回值:当且仅当执行完时返回 True
            说明:该方法仅保证返回表示 true 或 false 的 values,不一定返回 TrueFalse

        successful()
            功能:仅成功完成时返回 True,成功是指执行过程中没有出错
            参数:无
            返回值:TrueFalse
        
        get(block=True, timeout=None)
            功能:获取协程返回的结果或重新引发协程引发的异常
            参数:block 表示是否阻塞,timeout 表示阻塞的秒数
            返回值:Greenlet 对象的执行结果
            说明:当无法获取到 Greenlet 的结果时(如 block=False时协程没有执行完;block=True时阻塞时间结束但协程仍没有执行完)将引发 gevent.Timeout 异常
                 如果在 Greenlet 执行过程中引发了异常,该方法将会再次引发此异常

特别介绍(重点)

		spawn(func, *args, **kwargs)
            功能:创建一个新的 Greenlet 对象,并将其安排为运行 function(*args, **kwargs)
            
            参数:func 是一个可调用对象,表示协程要执行的任务
                 *args 和 **kwargs 是传入 func 的位置参数和关键字参数

            返回值:一个 Greenlet 对象
            
            说明:这是一个类方法,可用作 gevent.spawn() 和 Greenlet.spawn()
                 相当于 g1 = Greenlet()
                       g1.start()


        joinall(greenlets, timeout=None, raise_error=False, count=None)
            功能:等待 greenlets 序列中的所有 Greenlet 对象结束或超时
            
            参数:greenlet 表示一个序列
                 timeout 表示阻塞的时间
                 
            返回值:超时前所完成的 Greenlet 的执行结果序列(如果有)
            
            说明:1、每一个结果都是一个 Greenlet 对象,可调用 get() 方法
            	 2、这是 gevent 模块中的函数,可用作 gevent.joinall()

代码示例

import gevent


def work():
    print('切换至另一任务执行')
    gevent.sleep(3)
    print('另一任务结束')
    return 'work_over'


def main():
    print('一个任务开始')
    gevent.sleep(1)
    print('一个任务执行中')
    gevent.sleep(2)
    print('一个任务结束')
    return 'main_over'


if __name__ == '__main__':
    # g1 = gevent.Greenlet(run=main)
    # g2 = gevent.Greenlet(run=work)
    # g1.start()
    # g2.start()
    g1 = gevent.spawn(main)
    g2 = gevent.spawn(work)
    # 两句代替四句
    lis = gevent.joinall([g1, g2])
    for item in lis:
        print(item.name, '执行结果:', item.get())

  上面的代码已经能自动检测 IO 并切换了,但它其实还没什么实际意义,因为它只能检测 gevent 模块内的 IO(如 gevent.sleep())。要想让它识别 gevent 模块以外的 IO 阻塞,还需要打一个补丁,即导入 gevet 模块下的 monkey,并调用 monkey.patch_all() 方法。该方法必须在所有被打补丁的模块导入之前调用(如 time 模块),它的作用是将所有阻塞 IO 转化为 非阻塞 IO。下面是修改后的代码

import gevent
from gevent import monkey
monkey.patch_all()
import time


def work():
    print('切换至另一任务执行')
    time.sleep(3)
    print('另一任务结束')
    return 'work_over'


def main():
    print('一个任务开始')
    time.sleep(1)
    print('一个任务执行中')
    time.sleep(2)
    print('一个任务结束')
    return 'main_over'


if __name__ == '__main__':
    # g1 = gevent.Greenlet(run=main)
    # g2 = gevent.Greenlet(run=work)
    # g1.start()
    # g2.start()
    g1 = gevent.spawn(main)
    g2 = gevent.spawn(work)
    # 两句代替四句
    lis = gevent.joinall([g1, g2])
    for item in lis:
        print(item.name, '执行结果:', item.get())

协程的优点和缺点

优点

  1. 协程具有极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

  2. 不需要多线程的锁机制。因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

  3. 增加了程序并发度。协程时在单个线程内的并发执行,所以在整体上增加了整个程序的并发度

缺点

  1. 协程的本质是单线程,无法利用多核,但可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
  2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

Coroutines and Tasks

  贴个网址先,有空再看。官方文档

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值