Python并发编程2

5.协程

5.1 协程的概念

子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序
Coroutine协程,又可以成为微线程。

线程是系统级别的,是由系统来统一调度的;而协程是程序级别的由开发者根据自己的需要来调度的。同一线程下的一段代码执行的时候,可以中断,跳去执行另一段代码,当再次回来执行代码块的时候,可以接着从之前的地方开始执行,其实就是生成器。
协程不同于线程,线程是抢占式的调度,而协程是协同式的调度,协程需要自己做调度
协程拥有自己的寄存器上下文和栈,协程调度切换的时候,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复之前保存的寄存器上下文和栈。所以协程能够保留上一次调用的状态,当每次过程重新载入的时候,就相当于进入上一次调用的状态,也就是进入上一次离开时所处逻辑流的位置。

因为协程是一个线程中执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
例如:遇到IO密集型业务的时候,cpu总是处于闲置状态,所以我们使用多进程加上协程,让磁盘进行IO,cpu去做其他的任务,特别在web中效果更明显。

协程的优势:

  • 协程优势是极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。用来执行协程多任务非常合适。
  • 协程没有线程的安全问题。一个进程可以同时存在多个协程,但是只有一个协程是激活的,而且协程的激活和休眠又程序员通过编程来控制,而不是操作系统控制的。

总结如下
协程的优点

  • 无需线程上下文切换的开销,协程避免了无意义的调度,由此可以提高性能(但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力)
  • 无需原子操作锁定及同步的开销
  • 方便切换控制流,简化编程模型
  • 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

协程的缺点

  • 无法利用多核资源:协程的本质是个单线程,它不能同时将多核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  • 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

5.2 生成器函数—协程发展史

Python2.2第一次引入了生成器。当时的生成器引入了一种惰性,多次取值的方法,此时还是通过next构造生成器迭代链或者next进行多次取值。到了python2.5,yield关键字被加入到语法中,这样,生成器就有了记忆功能,下一次从生成器中取值可以恢复到生成器上一次yield执行的位置。而且,这时候的生成器还加入来了send方法,与yield搭配使用。也就是这时候的生成器不仅仅可以yield暂停到一个状态,还可以往它停止的位置通过send方法传入一个值改变其状态。

5.3 协程的实现

5.3.1 使用生成器yield实现

.协程的四个状态:

  • GEN_CREATED #协程等待开始执行
  • GEN_RUNNING # 协程正在运行
  • GEN_SUSPENDED #协程遇到yield停止
  • GEN_CLOSED # 协程执行结束

注意,最先调用 next(sc) 或send(None)函数这一步通常称为“预激”,只有当协程处于 GEN_SUSPENDED 状态下时才会运作。所以必须先调用一次。

** 使用生成器函数定义协程**
跟创建生成器类似,只是yield关键字右边没有表达式。

协程的异常和关闭

  • generator.close() 关闭协程
  • generator.throw() 抛出异常

这个方法能够让生成器在暂停的yield表达式处抛出指定的异常。如果生成器处理了抛出的异常,代码会向前执行到下一个yield表达式。而产出的值会成为调用throw方法得到的返回值。如果没有处理则向上抛。

协程的返回值
生成器处理了抛出的异常,如果在调用端异常做了处理,那么代码会继续向后执行,在异常处理中可以获得生成器的返回值。所以:注意这个返回值, 只能在异常中使用。

yield from
代替for循环
⽣成器有的时候,一个生成器函数需要产出另一个生成器生成的一系列值,传统的解决方法是使嵌套的 for 循环。而如果元素比较多,那么使用for循环,也值得,如果只有两个元素,也要写个for循环,比较麻烦,Next或者send一次只能取一个。Python3.3之后引入了yield from。yield from相当于简化了原来的for循环,同时可以将原来生成器中的返回值、异常等都直接引过来。

高级应用
yield from也可以看成是要调用另外一个生成器,方便的获得生成器的返回值。yield from 可以直接抓取StopItration 异常并将异常对象的 value 赋值给调用方的变量(最好是个序列型的数据类型,否则只能获得最后一个值,不会追加)

yield from的意义

  • 子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。
  • 使用 send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是None,那么会调用子生成器的 next() 方法。如果发送的值不是 None,那么会调用子生成器的 send() 方法。如果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。
  • 生成器退出时,生成器(或子生成器)中的 return expr 表达式会触发 StopIteration(expr) 异常抛出。
    vyield from 表达式的值是子生成器终止时传给 StopIteration 异常的第一个参数。
    v传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成器的 throw() 方法。如果调用 throw() 方法时抛出 StopIteration 异常,委派生成器恢复运行。 StopIteration 之外的异常会向上冒泡,传给委派生成器。
5.3.2 greenlet实现协程

前面的yield能实现协程,不过实现过程不易于理解,greenlet是在这方面做了改进。

5.3.3 gevent 实现协程

greenlet可以实现协程,不过每一次都要人为的去指向下一个该执行的协程,显得太过麻烦。时代在发展,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent。
gevent 是一个第三方库,可以轻松通过gevent实现协程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
涉及到的几个重要方法:
g1 = gevent.spawn(A) # 创建一个协程,其实就是Greenlet.spawn,创建一个协程
g1.join() 主线程要等待协程执行结束才结束。
Join是将单个greenlet加入运行队列中。
joinall是将创建的协程都加入

5.3.4 Asyncio

这个需要使用到python的Asyncio(async I o)模块,在asyncio模块详细讲述。

5.4 Asyncio模块

Async IO,异步IO。所谓异步 IO,就是你发起一个 IO 操作,不用等它结束,你可以继续做其他事情,当它结束时,你会得到通知。Asyncio 并不能带来真正的并行。当然,因为 GIL(全局解释器锁)的存在,Python 的多线程也不能带来真正的并行。但是我们可以通过一个线程中并行多个协程来提高运行效率。协程可以交给 asyncio 执行的任务并行执行。

5.4.1 重要概念和相关方法

coroutine 协程函数:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。

  • task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,包含任务的各种状态。
  • future: 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别
  • async关键字:,python3.5 用于定义协程的关键字,async定义一个协程
  • await 关键字:await用于挂起阻塞的异步调用接口。
  • get_event_loop 事件循环:程序开启一个无限的循环,程序员会把一些函数注册到事件循环上。当满足事件发生的时候,调用相应的协程函数。简单来说,只有 loop 运行了,协程才可能运行。
  • Iscoroutinefunction:判断是否是协程函数
  • Sleep:让协程睡一会;此时会cpu会自动切换到其他协程上运行
  • ensure_future:将协程(可能是多个协程函数)加入要执行的任务中,要执行的任务称为future(即将要执行的任务,未来任务)
  • loop.run_until_complete:将future任务加入到事件循环上之后,以阻塞形式调用,执行事件循环,直到协程任务运行结束,它才返回。注意,run_until_complete 的参数是一个 future,而且只接受一个future。
  • add_done_callback:向任务future中加入回调函数。
5.4.2 定义协程

使用async关键字定义协程。
协程可以:

  • 等待一个任务 future 结束
  • 等待另一个协程(产生一个结果,或引发一个异常)
  • 产生一个结果给正在等它的协程
  • 引发一个异常给正在等它的协程

Await关键字,等待另一个协程

5.4.3 运行协程

运行协程,有两种方式:

  • 在另一个已经运行的协程中用”await” 等待它
  • 通过”ensure_future”函数计划它的执行
5.4.4 回调函数

假如协程是一个 IO 的操作,不是我们简单的sleep,那么等它读完数据后,我们希望得到通知,以便下一步数据的处理。这一需求可以通过add_done_callback方法往 future任务中添加回调来实现。我们可以在加入future任务的时候,定义变量接到返回值,然后通过变量来完成对回调函数的调用。

5.4.5 多个协程并行

gather函数起聚合的作用,把多个 futures 包装成单个 future,因为 loop.run_until_complete 只接受单个 future。

直接调用gather函数:

gather(asyncio.ensure_future(协程1),asyncio.ensure_future(协程2)……,asyncio.ensure_future(协程n)

将协程放在列表中:

fs = [asyncio.ensure_future(协程1),
        asyncio.ensure_future(协程2),
        asyncio.ensure_future(协程3)]
gather(*fs)

获得协程的返回值:
run_util_complete(future)可以直接返回每个协程的返回值,但是是以生成器的方式返回。所以只能通过for遍历。

wait函数
Wait函数跟gather函数达到的效果基本类似。但传递参数的时候可以直接接受list,不用拆装包,同时返回一个将他们全包括在内的future。另外跟gather相比,出错的时候会使用try except形式。

5.4.6 run_until_complete和run_forever

run_forever也能达到这种效果,但是,它需要协程stop才能停止调用,否则一直等待调用下一个协程。

5.4.7 close loop

清除loop循环对象。建议调用 loop.close,以彻底清理 loop 对象防止误用。

5.4.8 关于@asyncio.coroutine

以装饰器的形式来声明和定义协程。

5.4.9 协程的嵌套

在asyncio操作协程的过程中,协程生成器中可以通过await继续调用其他协程

5.4.10 注意事项:
  • async def函数必定是协程函数,不管里面有没有await语句
  • 但是在async def函数中不能使用yield和yield from语句,会引发语法错误
  • 如果让协程睡一会,使用asyncio中的sleep。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值