Python中协程(coroutine)的初步认识

Blog地址:https://www.jiangdog.com/blog/yield-and-coroutine

基本概念的认识

  1. 之前在浏览相关文章gevent源码分析时对协程和进程做了相关比较。

    • 相同点:二者都是可以看做是一种执行流, 该执行流可以挂起,并且在将来又可以在 你挂起的地方恢复执行, 这实际上都可以看做是continuation, 我们来看看当我们挂 起一个执行流时我们要保存的东西
      1. 栈, 因为如果你不保存栈,那么局部变量你就无法恢复,同时函数的调用链你也无 法恢复,
      2. 寄存器的状态: 这好理解, 比如说EIP,如果你不保存,那么你恢复执行流就不知道 到底执行哪一条指令, 在比如说ESP,EBP, 如果你不保存,那么你即便有完整的栈 你也不知道怎么用.
        这二者实际就是所谓的上下文,也可以说是continuation. 在执行流切换时必须保存 这两个东西, 内核调度进程时也是一回事.
    • 不同点:
      1. 执行流的调度者不同, 进程是内核调度, 而协程是在用户态调度, 也就是说进程 的上下文是在内核态保存恢复的,而协程是在用户态保存恢复的. 很显然用户态的 代价更低
      2. 进程会被抢占,而协程不会,也就是说协程如果不主动让出CPU,那么其他的协程是不 可能得到执行机会,这实际和早期的操作系统类似,比如DOS, 它有一个yield原语, 一个进程调用yield,那么它就会让出CPU, 其他的进程也就有机会执行了, 如果一 个进程进入了死循环,那么整个系统也就挂起了,永远无法运行其他的进程了, 但 对协程而言,这不是问题
      3. 对内存的占用不同,实际上协程可以只需要4K的栈就够了, 而进程占用的内存要大 的多.
      4. 从操作系统的角度讲, 多协程的程序是单线程,单进程的
  2. 《Fluent Python》中对协程有这样的解释:

    通过客户调用 .send(…) 方法发送数据或使用 yield from 结构驱动的生成器函数。

从生成器演变成协程

  1. 普通生成器generator通过调用.send(value)方法发送数据,且该数据将作为yield表达式的值,所有生成器能够作为协程使用。
  2. 还添加了.throw(),用于抛出调用方异常,在生成器中处理;.close(),用于终止生成器。
  3. yield from用于嵌套生成器。

协程的基本操作

  1. 协程的基本例子。

      def simple_coroutine():
          c = 0
          print('coroutine start')
          x = yield c
          print('get --> %s' % x)
          c += 1
          yield c
    
      sc = simple_coroutine()
      try:
          sc.send(1)
      except TypeError as e:
          print(e)  # can't send non-None value to a just-started generator
      # coroutime start
      print(next(sc))  # 0
      # get --> test
      print(sc.send('test'))  # 1
      try:
          next(sc)
      except StopIteration:
          print('coroutine end')    
    • 若创建协程后没有预激协程next()send(None),而是send()了一个非None的对象,则会抛出异常,当协程还未启动时候,是不能调用send()向其发送一个非None的数据。
    • 首次调用next()后,协程在yield后暂停,且产出yield后的值c,若无c,实际产出的是None
    • 当协程暂停时,此时我们调用send('test'),向协程发送数据test,此时协程恢复,yield表达式计算并得到数据test并赋值给x,协程恢复执行直至下一个yield处暂停,并产出该yield后的c的值,即1
    • 最后恢复执行协程,此时已经到了协程末尾,抛出StopIteration异常。
    • 协程的四种状态。GEN_CREATED等待开始执行、GEN_RUNNING正在执行、GEN_SUSPENDED在yield表达式处暂停、GEN_CLOSED结束。
      
      # 四种状态
      
      def simple_coroutine_for_state():
         print('start')
         x = yield 1
         print(inspect.getgeneratorstate(sc))  # GEN_RUNNING
         print('get -->%s' % x)
      
      sc = simple_coroutine_for_state()
      print(inspect.getgeneratorstate(sc))  # GEN_CREATED
      next(sc)
      print(inspect.getgeneratorstate(sc))  # GEN_SUSPENDED
      try:
         sc.send('test state')
      except StopIteration:
         print(inspect.getgeneratorstate(sc))  # GEN_CLOSED
  2. 预激协程的装饰器:

    • 如上所述,对于一个协程,send()方法的参数会成为yield表达式的值,仅当协程处于暂停状态时,才能调用send()非None方法,所以当协程处于未激活状态时,必须调用next()send()方法来激活协程。
    • 对于预激,《Fluent Python》中有如下描述:

      最先调用 next(my_coro) 函数这一步通常称为“预激”(prime)协程
      (即,让协程向前执行到第一个 yield 表达式,准备好作为活跃的协
      程使用)。

    • 简单的预激装饰器:

      def prime_decorator(func):
       @wraps(func)
       def wrapper(*args, **kwargs):
           g = func(*args, **kwargs)
           next(g)
           return g
       return wrapper
      
      
      @prime_decorator
      def cal_average():
       total = 0
       count = 0
       average = None
       while True:
           get = yield average
           total += get
           count += 1
           average = total / count
      
      cal_gen = cal_average()
      print(cal_gen.send(100))  # 100.0
      print(cal_gen.send(500))  # 300.0
    • Tornado中的gen.coroutine也是与预激协程有关:

      def coroutine(func, replace_callback=True):
          return _make_coroutine_wrapper(func, replace_callback=True)
      
       def _make_coroutine_wrapper(func, replace_callback):
           ······
           @functools.wraps(wrapped)
           def wrapper(*args, **kwargs):
               future = TracebackFuture()
               ······
               if isinstance(result, GeneratorType):
                   try:
                       orig_stack_contexts = stack_context._state.contexts
                       yielded = next(result)
                       ······
               ······
               return future
           ······
       return wrapper

      在预激装饰器内,除了简单的预激协程,也还能做一些其他更多的事情。

  3. 终止协程和异常处理

    • 终止协程:利用generator.close()方法来关闭协程,关闭后协程的状态变为GEN_CLOSED
    • 异常处理:利用generator.throw()使协程在暂停的yield表达式处抛出指定异常,若该异常被处理,则协程向下执行且返回值是yield的产出值;若该异常未被处理,则向上冒泡,传到调用方的上下文中,此时协程也会停止。
         class DemoException(Exception):
            """"""
      
         def handle_exc_gen():
             print('start')
             yielded_value = 0
             try:
                 while True:
                     try:
                         x = yield yielded_value
                     except DemoException:
                         print('handle demo exception and continuing')
                         yielded_value = 0
                     else:
                         print('receive %s' % x)
                         yielded_value += 1
             finally:
                 print('end')
      
        exc_coro = handle_exc_gen()
        print(next(exc_coro))  # 预激协程 start  0
        print(exc_coro.send('test'))  # receive test   1
        print(exc_coro.send('throw'))  # receive throw  2
        res = exc_coro.throw(DemoException)  # handle demo exception and continuing
        print(res)  # 0,处理DemoException时 yieled_value重新变为0
        # 抛出未处理异常
        exc_coro.throw(TypeError)
  4. 让协程返回值:在Python3.3之后可以在协程中写return语句而不会产生语法错误。此时协程返回数据的流程是:协程运行结束终止->抛出StopIteration异常,异常对象的value属性保存着协程的返回值->补货StopIteration并获得返回值。

       def get_average():
           count = 0
           total = 0.0
           average = None
           while True:
               get = yield average
               if get is None:
                   break
               total += get
               count += 1
               average = total / count
           return total, count
    
       get_average_coro = get_average()
       next(get_average_coro)
       get_average_coro.send(10)
       get_average_coro.send(30)
       try:
           get_average_coro.send(None)
       except StopIteration as e:
           print(e)  # (40.0, 2)

    依旧符合生成器对象的行为:耗尽后抛出StopIteration异常。

认识yield from

  1. 简单使用yield from

    • 简化for循环中的yield

         # 简化for循环中的yield
         def for_gen():
             for s in 'ABC':
                 yield s
             for i in range(1, 3):
                 yield i
      
         for_g = for_gen()
         print(list(for_g))  # ['A', 'B', 'C', 1, 2]
      
      
         def simplify_for_gen():
             yield from 'ABC'
             yield from range(1, 3)
      
         simplify_for_g = simplify_for_gen()
         print(list(simplify_for_g))  # ['A', 'B', 'C', 1, 2]
    • 连接可迭代对象

       def chain(*iterables):
           for it in iterables:
               yield from it
      
       s = 'ABC'
       n = range(1, 3)
       print(list(chain(s, n)))  # ['A', 'B', 'C', 1, 2]
      
      
       def flatten(items, ignore_types=(str, bytes)):
           for x in items:
               if isinstance(x, collections.Iterable) and not isinstance(x, ignore_types):
                   yield from flatten(x)
               else:
                   yield x
      
       items = ['a', ['b1', 'b2'], [['c11', 'c12'], ['c21', 'c22']]]
       print(list(flatten(items)))  # ['a', 'b1', 'b2', 'c11', 'c12', 'c21', 'c22']   
  2. 其他

小结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值