zoukankan      html  css  js  c++  java
  • python 协程

    协程:又名微线程,纤程。英文名Coroutine,底层是用生成器yield 实现。同步异步讲解

    子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

    协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行

    注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。比如子程序A、B

    def A():
        print('1')
        print('2')
        print('3')
    
    def B():
        print('x')
        print('y')
        print('z')

    假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:

    1
    2
    x
    y
    3
    z

    但是在A中是没有调用B的,所以协程的调用比函数调用理解起来要难一些。

    看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

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

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

    因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能

    Python对协程的支持是通过generator实现的。

    在generator中,我们不但可以通过for循环来迭代,还可以不断调next()函数获取由yield语句返回的下一个值

    但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数

    来看例子:

    传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。

    如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

    def consumer():
        r = ''
        while True:
            n = yield r
            if not n:
                return
            print('[CONSUMER] Consuming %s...' % n)
            r = '200 OK'
    
    def produce(c):
        c.send(None)
        n = 0
        while n < 5:
            n = n + 1
            print('[PRODUCER] Producing %s...' % n)
            r = c.send(n)
            print('[PRODUCER] Consumer return: %s' % r)
        c.close()
    
    c = consumer()
    produce(c)

    执行结果:

    [PRODUCER] Producing 1...
    [CONSUMER] Consuming 1...
    [PRODUCER] Consumer return: 200 OK
    [PRODUCER] Producing 2...
    [CONSUMER] Consuming 2...
    [PRODUCER] Consumer return: 200 OK
    [PRODUCER] Producing 3...
    [CONSUMER] Consuming 3...
    [PRODUCER] Consumer return: 200 OK
    [PRODUCER] Producing 4...
    [CONSUMER] Consuming 4...
    [PRODUCER] Consumer return: 200 OK
    [PRODUCER] Producing 5...
    [CONSUMER] Consuming 5...
    [PRODUCER] Consumer return: 200 OK

    要看懂上面的例子,关键在于要理解下面几点:

    1、例子中的c.send(None),其功能类似于next(c),比如:

    In [5]: def num():
       ...:     yield 1
       ...:     yield 2
       ...:     
    
    In [6]: c = num()
    
    In [7]: c.send(None)
    Out[7]: 1
    
    In [8]: c.send(None)
    Out[8]: 2
    
    In [9]: c.send(None)
    ---------------------------------------------------------------------------
    StopIteration                             Traceback (most recent call last)
    <ipython-input-9-1f377d70f7a5> in <module>()
    ----> 1 c.send(None)
    
    StopIteration: 
    
    In [10]: 
    

    2、n = yield r,这里是一条语句,但要理解两个知识点,赋值语句先计算 = 右边,由于右边是 yield 语句,所以yield语句执行完以后,进入暂停,而赋值语句在下一次启动生成器的时候首先被执行

    3、send 在接受None参数的情况下,等同于next(generator)的功能,但send同时也可接收其他参数,比如例子中的c.send(n),要理解这种用法,先看一个例子:

    In [10]: def num():
        ...:     a = yield 1
        ...:     while True:
        ...:         a = yield a
        ...:         
    
    In [11]: c = num()
    
    In [12]: c.send(None)
    Out[12]: 1
    
    In [13]: c.send(5)
    Out[13]: 5
    
    In [14]: c.send(100)
    Out[14]: 100
    
    In [15]: 
    

    在上面的例子中,首先使用 c.send(None),返回生成器的第一个值,a = yield 1 ,也就是1(但此时,并未执行赋值语句),

    接着我们使用了c.send(5),再次启动生成器,并同时传入了一个参数 5,再次启动生成的时候,从上次yield语句断掉的地方开始执行,即 a 的赋值语句,由于我们传入了一个参数 5所以a被赋值为5,接着程序进入whlie循环,当程序执行到 a = yield a,同理,先返回生成器的值 5,下次启动生成器的时候,再执行赋值语句,以此类推...

    所以c.send(n)的用法就是老师上文中所说的 ," Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。"

    但注意,在一个生成器函数未启动之前,是不能传递值进去。也就是说在使用c.send(n)之前,必须先使用c.send(None)或者next(c)来返回生成器的第一个值

    最后我们来看上文中的例子,梳理下执行过程:

    第一步:执行 c.send(None),启动生成器返回第一个值,n = yield r,此时 r 为空,n 还未赋值,然后生成器暂停,等待下一次启动。

    第二步:生成器返回空值后进入暂停,produce(c) 接着往下运行,进入While循环,此时 n 为1,所以打印:

    [PRODUCER] Producing 1...

    第三步:produce(c) 往下运行到 r = c.send(1),再次启动生成器,并传入了参数1,而生成器从上次n的赋值语句开始执行,n 被赋值为1n存在,if 语句不执行,然后打印:

    [CONSUMER] Consuming 1...

    接着r被赋值为'200 OK',然后又进入循环,执行n = yield r,返回生成器的第二个值,'200 OK',然后生成器进入暂停,等待下一次启动。

    第四步:生成器返回'200 OK'进入暂停后,produce(c)往下运行,进入r的赋值语句,r被赋值为'200 OK',接着往下运行,打印:

    [PRODUCER] Consumer return: 200 OK

    以此类推...

    n5跳出循环后,使用c.close() 结束生成器的生命周期,然后程序运行结束。

    关注公众号 海量干货等你
  • 相关阅读:
    62. 不同路径-dp-中等
    49. 字母异位词分组-字符串-中等
    34. 在排序数组中查找元素的第一个和最后一个位置-二分查找-中等
    33. 搜索旋转排序数组-二分查找-中等
    221. 最大正方形-dp-中等难度
    38. 外观数列-字符串-中等难度
    mysql-索引/视图/存储过程/函数/触发器
    mysql-常用命令
    MDK5.34发布,AC6升级至V6.16(2021-03-19)
    STM32H7的CAN FD教程笔记
  • 原文地址:https://www.cnblogs.com/sowhat1412/p/12734208.html
Copyright © 2011-2022 走看看