Python多线程,协程,异步

问题

  1. 怎么引发stopiteration异常? —-》在生成器中用return;或在生成器中不用循环

tmp

  • https://mp.weixin.qq.com/s/GgamzHPyZuSg45LoJKsofA?
  • 的学习笔记,用yield from重构代码中
    1. 事件循环+回调
    2. OS将I/O状态的变化都封装成了事件,如可读事件、可写事件。并且提供了专门的系统模块让应用程序可以接收事件通知。python用selectors库
    3. 需事先知道代码执行到哪一块会阻塞

基础tips

  • 线程是最小的执行单元,而进程由至少一个线程组成
  • 一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,
    有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
  • 多任务的实现有3种方式:
    多进程模式;
    多线程模式;
    多进程+多线程模式。
  • 真正的并行执行多任务只能在多核CPU上实现
  • 真正地同时执行多线程需要多核CPU才可能实现。
  • python由于有GIL,就算有多核cpu,也只能跑一个线程,所以python在多核情况下要用多进程+协程
  • 在单核CPU下的多线程其实都只是并发,不是并行,并发和并行从宏观上来讲都是同时处理多路请求的概念。但并发和并行又有区别,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。
  • 同步IO模型的代码是无法实现异步IO模型的。
    异步IO模型需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程:
  • 用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。

线程

  • 在做阻塞的系统调用时,例如sock.connect(),sock.recv()时,当前线程会释放GIL,让别的线程有执行机会。但是单个线程内,在阻塞调用上还是阻塞的
  • Python中 time.sleep 是阻塞的,都知道使用它要谨慎,但在多线程编程中,time.sleep 并不会阻塞其他线程
  • 所有的多线程还有通病。它们是被OS调度,调度策略是抢占式的,以保证同等优先级的线程都有均等的执行机会,那带来的问题是:并不知道下一时刻是哪个线程被运行,也不知道它正要执行的代码是什么。所以就可能存在竞态条件
  • 例如爬虫工作线程从任务队列拿待抓取URL的时候,如果多个爬虫线程同时来取,那这个任务到底该给谁?那就需要用到“锁”或“同步队列”来保证下载任务不会被重复执行

协程

用作协程的生成器的基本行为

  • GEN_CREATED,GEN_RUNNING,GEN_SUSPENDED,GEN_CLOSED,详情见《流畅的python》
  • from inspect import getgeneratorstate getgeneratorstate(my_coro2)来检查生成器的状态
  • GEN_CLOSED,在调用close方法后,或遇到了stopiteration会出现这个状态
  • GEN_SUSPENDED 表示在 yield 表达式处暂停

stopiteration

  • 如果协程内没有处理异常,且遇到了异常时,协程会终止。如果试图重新激活协程,会抛出 StopIteration 异常。
  • 如果不管协程如何结束都想做些清理工作,要把协程定义体中相关的代
    码放入 try/finally 块中

生成器的throw方法

  • 如果生成器处理了抛出的异常,代码会向前执行到下一个 yield 表达式包括GeneratorExit

  • 如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中

    def myGenerator():
      try:
          yield 1
          yield 2
      except ZeroDivisionError:
          pass
    
    gen = myGenerator()
    print(next(gen))
    
    # print(gen.close())
    
    print(gen.throw(GeneratorExit))
  • 这段代码会报错,原因是,没有处理GeneratorExit异常,但是,调用close方法不会报错,因为close方法就算是生成器没有处理GeneratorExit异常,也不会报错。

  • 对于有except GeneratorExit 的情况:throw(GeneratorExit)后需要有yield,否则会报错StopIteration;但close方法调用后,生成器中不能有yield,否则会报RuntimeError。这一点,说明生成器的throw(GeneratorExit)和close不是完全等价的。

生成器的close方法

  • 致使生成器在暂停的 yield 表达式处抛出 GeneratorExit 异常,后续执行的语句中,不能再有yield语句,否则会产生RuntimeError,但是,若是直接通过throw一个GeneratorExit ,代码后面可以有yield
def myGenerator():
    try:
        yield 1
        yield 2
    except GeneratorExit:
        print('sd')
    # print('fin')
    # yield 11

def tt(q):
    yield from q

q=myGenerator()
gen = tt(q)
print(next(gen))
# gen.close()
gen.throw(GeneratorExit)
  • 这个例子说明,close方法和throw(GeneratorExit)方法有不同,这里调用close不会出错,但调用throw(GeneratorExit)会出错
  • 下面是《流畅的python》的原文
如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器
上调用 close() 方法,那么在子生成器上调用 close() 方法,如
果它有的话。如果调用 close() 方法导致异常抛出,那么异常会
向上冒泡,传给委派生成器;否则,委派生成器抛出
GeneratorExit 异常(就是说,在子生成器上调用玩close方法后,还会在委派生成器上throw一个GeneratorExit或调用close方法,具体用throw还是close,看最初传给委派生成器的是哪一个)。

GeneratorExit

  • 当一个生成器对象被销毁时,会引发close方法,或者其他方式引起的GeneratorExit,但不是通过throw引起的,因为1,生成器内没有捕捉GeneratorExit的话会报错,2,生成器内,捕捉GeneratorExit的话,会去返回下一个yield,而通过实验,发现若有下一个yield时,会报RuntimeError
  • close方法和throw引起GeneratorExit的不同:close方法,和throw都会引起GeneratorExit,但是调用close后,生成器的后面不能再由yield了;而通过throw出来的异常,并且还被捕获了,则会在生成器中找下一个yield
  • GeneratorExit异常继承自BaseException类。BaseException类与Exception类不同。一般情况下,BaseException类是所有内建异常类的基类,而Exception类是所有用户定义的异常类的基类。
  • 主程序结束,若没有给生成器用close方法,当脚本运行完成后,会调用close方法。而GeneratorExit异常产生的时机,是在生成器对象被销毁之前
  • 生成器对象的close方法会在生成器对象方法的挂起处抛出一个GeneratorExit异常。GeneratorExit异常产生后,系统会继续把生成器对象方法后续的代码执行完毕

yield from

  • 使用 yield from 句法(参见 16.7 节)调用协程时,会自动预激【这里的预激是指隐式预激,可以判流畅的python的16.8的介绍yield from的伪代码:】,因此与示例 16-5 中的 @coroutine 等装饰不兼容。

    from functools import wraps
    
    def coroutine(func):
      @wraps(func)
      def primer(*args,**kwargs):
          gen = func(*args,**kwargs)
          next(gen)
          return gen
      return primer
    
    @coroutine
    def averager2():
      total = 0.0
      count = 0
      average = None
      while True:
          term = yield average
          total += term
          count += 1
          average = total/count
    
    def grouper(c):
      yield from c
    
    g=averager2()
    g=grouper(g)
    
    next(g)
    print(g.send(1))
    print(g.send(2))
    • 这里,无论代码的最后有无next(g),都会报错,有next(g)的话,由于g已经预激了,会None送给term;没有next(g)的话,虽然averager2已经预激,但是被grouper嵌套后,yield from的实现里面,需要先next一下,不然会报can’t send non-None value to a just-started generator
  • Python 3.4 标准库里的 asyncio.coroutine 装饰器(第 18 章介绍)不会预激协程,因此能兼容 yield from 句法。【指用asyncio.coroutine 装饰器得到的协程,不能用yield from 调用?】

  • 我们无法在控制台中使用交互的方式测试这种行为,因为在函数外部使用 yield from(以及 yield)会导致句法出错。
  • yield from x 表达式对 x 对象所做的第一件事是,调用 iter(x),从中获取迭代器。因此,x 可以是任何可迭代的对象。
  • xxx = yield from yyy; yyy是子生成器,xxx的值是yyy中 子生成器终止时传给StopIteration异常的第一个参数
  • 把各个 value 传给 grouper(调用方send数值)。传入的值最终到达 averager 函数中term = yield 那一行;grouper 永远不知道传入的值是什么。
  • 调用方 main;委派生成器 (有yield from的)grouper;子生成器 (有yield 的)averager。
  • 委派生成器相当于管道,所以可以把任意数量个委派生成器连接在一起:一个委派生成器使用 yield from 调用一个子生成器,而那个子生成器本身也是委派生成器,使用 yield from 调用另一个子生成器
  • 如果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器

    别人的总结

    1. 迭代器(即可指子生成器)产生的值直接返还给调用者 
    2. 任何使用send()方法发给委派生产器(即外部生产器)的值被直接传递给迭代器。如果send值是None,则调用迭代器next()方法;如果不为None,则调用迭代器的send()方法。如果对迭代器的调用产生StopIteration异常,委派生产器恢复继续执行yield from后面的语句;若迭代器产生其他任何异常,则都传递给委派生产器。 
    3. 除了GeneratorExit 异常外的其他抛给委派生产器的异常,将会被传递到迭代器的throw()方法。如果迭代器throw()调用产生了StopIteration异常,委派生产器恢复并继续执行,其他异常则传递给委派生产器
    4. 如果GeneratorExit异常被抛给委派生产器,或者委派生产器的close()方法被调用,如果迭代器有close()的话也将被调用。如果close()调用产生异常,异常将传递给委派生产器。否则,委派生产器将抛出GeneratorExit 异常。 
    5. 当迭代器结束并抛出异常时,yield from表达式的值是其StopIteration 异常中的第一个参数。 
    6. 一个生成器中的return expr语句将会从生成器退出并抛出 StopIteration(expr)异常。

asyncio

  • event_loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足条件发生的时候,就会调用对应的处理方法。
  • coroutine:中文翻译叫协程,在 Python 中常指代为协程对象类型,我们可以将协程对象注册到时间循环中,它会被事件循环调用。我们可以使用 async 关键字来定义一个方法,这个方法在调用时不会立即被执行,而是返回一个协程对象。
  • task:任务,它是对协程对象的进一步封装,包含了任务的各个状态。
  • future:代表将来执行或没有执行的任务的结果,实际上和 task 没有本质区别。

普通的方式

async def xxx:
    xxx
  • async 关键字来定义一个方法,这个方法在调用时不会立即被执行,而是返回一个协程对象。
import asyncio

async def execute(x):
    print('Number:', x)

coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')

loop = asyncio.get_event_loop()
loop.run_until_complete(coroutine)
print('After calling loop')

创建task

  • 方法一:
loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)
loop.run_until_complete(task)
  • 方法二
task = asyncio.ensure_future(coroutine)
print('Task:'task)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)

绑定回调

为某个 task 绑定一个回调方法

# 参数是task对象,task.result()是task绑定函数return的结果
def callback(task):
    print('Status:', task.result())


task.add_done_callback(callback)

多任务协程

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值