前言

都知道python中因为全局解释器锁的原因,导致python的多线程无法高效的利用到多核的性能。如果想要高效利用多核的优势,只能使用多进程。

多线程同时还会有个问题,就是共性一个内存数据,如果对多个线程对同一数据进行读写,没有加上LOCK的话,会造成错误。如果其中一个线程发生问题,会导致整个程序的崩溃。例如IIS服务就是多线程服务。

多进程因为每个进程都有一把GIL所以能高效利用多核的优势,但是多个进程同样会给系统的负载带来压力。常见的Apache服务就是多进程的,所以IIS的稳定性不如Apache。

那么,有没有更加高效的处理模式呢?答案就是协程

协程

举个例子来说,Nginx就是通过单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型。那么单线程的异步编程模型就叫做协程。协程相比于多线程,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

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

当我们需要更高的效率的时候,可以利用多进程+协程的编程模式实现。

实现

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)
    for n in range(1,6):
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()

c = consumer()

produce()

协程的实现方式是通过生成器来实现的,Consumer就是一个生成器,需要补充一个知识点

send()

通常c = consumer()返回了一个iterator对象,通常可以通过next()函数来到达写一次yield产生点,这里通过了send()这个函数,举个简单的例子来说明

def test_iter():
    while True:
        n = yield 1
        print("receive" + str(n))

g = test_iter()
print(next(g))
print(g.send('test'))
print(next(g))

它的结果为

1
receivetest
1
receiveNone
1

send()函数简单来说在完成next()函数功能的基础上,同时也向生成器函数发送了数据,也就是这里n接受到的值。

讲完了send函数就可以继续理解我们的协程了,Consumer是个生成器函数,c是个iterator对象。
1. produce函数将c作为参数传入
1. c.send(None)启动了生成器,停止在了n = yield r
1. produce函数里面通过c.send(n)将n传入生成器之中,同时启动生成器,直到遇到下一个yield
1. 最后close()

有几个注意点:
- 为何要在一开始send(None),因为TypeError: can't send non-None value to a just-started generator这个错误表示不允许传入一个非零值给一个刚开始的生成器,也就是说,在一个生成器函数未启动之前,是不能传递数值进去。必须先传递一个None进去或者调用一次next(g)方法,才能进行传值操作。
- 一开始我犯了个错误,for n in range(5):然后代码抛出异常StopIteration,我刚开始想到这个异常不是生成器执行到结束的时候会抛出吗,怎么我刚开始就抛出了,然后一看Consumer函数,if not n: return 你懂得
- close()这个方法会在生成器对象方法的挂起处抛出一个GeneratorExit异常。GeneratorExit异常产生后,系统会继续把生成器对象方法后续的代码执行完毕。

总结

可以看到协程在整个流程中都是无锁的,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

更新

yield from 关键点

  • 子生成器生产的值都是直接传给调用方的;调用方通过.send()方法发送的值都是直接传递给子生成器的;如果发送的是None,会调用子生成器的next()方法,如果发送的不是None,会调用子生成器的.send()方法
  • 子生成器退出的时候,最后的return Expr,会触发一个StopIteration(Expr)的异常;
  • yield from表达式的值是子生成器终止时,传递给StopIteration异常的第一个参数,也就是return的值;
  • 如果调用的时候出现StopIteration异常,委托生成器会恢复运行,同时其他的“异常”会向上冒泡;
  • 传入委托生成器的异常里,除了GenerationExit之外,其他的所有异常全部传给子生成器的.throw()方法;如果调用throw()出现StopIteration异常,那么就恢复委托生成器的运行,其他的异常全部向上冒泡;
  • 如果在委托生成器调用.close()或者传入GenerationExit异常,会调用子生成器的.close()方法,没有的话就不调用。如果在close()的时候抛出了异常,就向上冒泡,否则的话委托生成器会抛出GenerationExit异常。

版权声明:本文为原创文章,版权归 heroyf 所有。
本文链接: https://heroyf.club/2019/07/python_coroutine/


“苹果是给那些为了爱选择死亡的人的奖励”