gevent 学习笔记 —— 协程

在学习gevent以前,你确定要知道你学的这个东西是什么。git

官方描述gevent

gevent is a coroutine-based Python networking library that uses greenlet to provide a high-level synchronous API on top of the libev event loop.程序员

翻译:gevent是一个基于协程的Python网络库。咱们先理解这句,也是此次学习的重点——协程github

wiki描述协程

与子例程同样,协程也是一种程序组件。相对子例程而言,协程更为通常和灵活,但在实践中使用没有子例程那样普遍。子例程的起始处是唯一的入口点,一旦退出即完成了子例程的执行,子例程的一个实例只会返回一次;协程能够经过yield来调用其它协程。经过yield方式转移执行权的协程之间不是调用者与被调用者的关系,而是彼此对称、平等的。协程容许多个入口点,能够在指定位置挂起和恢复执行。安全

没看懂?不要紧,我也没看懂,不过算是有点线索:子例程网络

子例程

过程有两种,一种叫子例程(Subroutine),一般叫Sub;另外一种叫函数(Function)。底层实现机制是同样的,区别在于,Sub只执行操做,没有返回值;Function不但执行操做,而且有返回值。用过VB的应该会比较清楚这点。(原谅我用了百度百科)说到底子例程就是过程,咱们通常叫它函数。多线程

说到函数,我就想吐槽了,不明白为何要叫函数。不少时候咱们写一个函数是为了封装、模块化某个功能,它是一个功能、或者说是一个过程。由于它包含的是相似于流程图那样的具体逻辑,先怎样作,而后怎样作;若是遇到A状况则怎样,若是遇到B状况又怎样。我的以为仍是叫过程比较好,叫作函数就让人很纠结了,难道由于回归到底层仍是计算问题,出于数学的角度把它称为函数?这个略坑啊!为了符合你们的口味,我仍是称之为函数好了(其实我也习惯叫函数了%>_<%)。并发

讲到函数,咱们就往底层深刻一点,看看下面的代码:dom

def a():
    print "a start"
    b()
    print "a end"

def b():
    print "b start"
    c()
    print "b end"

def c():
    print "c start"
    print "c end"

if __name__ == "__main__":
    a()

a start
b start
c start
c end
b end
a end

对于这样的结果你们确定不会意外的。每当函数被调用,就会在栈中开辟一个栈空间,调用结束后再回收该空间。ide

假设一个这样的场景:有个讲台,每一个人均可以上去发表言论,可是每次讲台只能站一我的。如今a在上面演讲,当他说到“你们好!”的时候,b有个紧急通知要告诉你们,因此a就先下来让b讲完通知,而后a再上讲台继续演讲。若是用函数的思想模拟这个问题,堆栈示意图是这样的:模块化

你们会不会发现问题,就是b通知完a继续演讲都要从新开始。由于函数在从新调用的时候,它的局部变量是会被重置的,对于以前他说的那句“你们好”,他是不会记得的(可能a的记性很差)。那有没有什么办法能够不让他重复,而是在打断以后继续呢?很简单,在他走下讲台以前记住当前说过的话。表如今函数中就是在退出以前,保存该函数的局部变量,方便在从新进入该函数的时候,可以从以前的局部变量开始继续执行。

升级版

若是你有一段代码生产数据,另一段代码消费数据,哪一个应该是调用者,哪一个应该是被调用者?

例如:生产者 —— 消费者问题,先抛开进程、线程等实现方法。假设有两个函数producer和consumer,当缓冲区满了,producer调用consumer,当缓冲区空了,consumer调用producer,可是这样的函数互相调用会出什么问题?

def producer():
    print "生产一个"
    consumer()

def consumer():
    print "消费一个"
    producer()

producer生产一个,缓冲区满了,consumer消费一个,缓冲区空了,producer生产一个,如此循环。会看到下面这样的图:

看起来好像不错,感受两个函数协调运行的很好,很好的解决了生产者——消费者问题。若是真有这么好也就不会有协程的存在了,仔细分析会有两个问题:

  1. 无限次数的函数嵌套调用,而没有函数返回,会有什么样的后果?
  2. 两个函数貌似协调有序的工做,你来我往,但每次执行的都是同一个函数实例吗?

首先,上面的伪代码示例是一个无限的函数嵌套调用,没有函数返回来释放栈,栈的空间不断的在增加,直到溢出,程序崩溃。而后,看起来两个函数协调有序,事实上操做的都不是同一个实例对象,不知道下面的图可否看懂。

那什么东西有这样的能力呢?咱们很快就能够想到进程、线程,可是你真的想使用进程、线程如此重量级的东西在这么简单的程序上吗?野蛮的抢占式机制和笨重的上下文切换!

还有一种程序组件,那就是协程。它能保留上一次调用时的状态,每次从新进入该过程的时候,就至关于回到上一次离开时所处逻辑流的位置。协程的起始处是第一个入口点,在协程里,返回点以后是接下来的入口点。协程的生命期彻底由他们的使用的须要决定。每一个协程在用yield命令向另外一个协程交出控制时都尽量作了更多的工做,放弃控制使得另外一个协程从这个协程中止的地方开始,接下来的每次协程被调用时,都是从协程返回(或yield)的位置接着执行。

从上面这些你就能够知道其实协程是模拟了多线程(或多进程)的操做,多线程在切换的时候都会有一个上下文切换,在退出的时候将现场保存起来,等到下一次进入的时候从保存的现场开始,继续执行。

看下协程是怎样实现的:

import random
from time import sleep
from greenlet import greenlet
from Queue import Queue

queue = Queue(1)

@greenlet
def producer():
    chars = ['a', 'b', 'c', 'd', 'e']
    global queue
    while True:
        char = random.choice(chars)
        queue.put(char)
        print "Produced: ", char
        sleep(1)
        consumer.switch()

@greenlet
def consumer():
    global queue
    while True:
        char = queue.get()
        print "Consumed: ", char
        sleep(1)
        producer.switch()

if __name__ == "__main__":
    producer.run()
    consumer.run()

应用场景

咱们一直都在大谈协程是什么样一个东西,却从没有提起协程用来干吗,这个其实你们分析一下就可以知道。从上面的生产者——消费者问题应该能看出,它分别有两个任务,假设交给两我的去执行,但每次只能容许一我的行动。当缓冲区满的时候,生产者是出于等待状态的,这个时候能够将执行任务的权利转交给消费者,当缓冲区空得时候,消费者是出于等待状态的,这个时候能够将执行任务的权利转交给生产者,是否是很容易联想到多任务切换?而后想到线程?最后想到高并发

但同窗们又会问,既然有了线程为何还要协程呢?由于线程是系统级别的,在作切换的时候消耗是特别大的,具体为何这么大等我研究好了再告诉你;同时线程的切换是由CPU决定的,可能你恰好执行到一个地方的时候就要被迫终止,这个时候你须要用各类措施来保证你的数据不出错,因此线程对于数据安全的操做是比较复杂的。而协程是用户级别的切换,且切换是由本身控制,不受外力终止。

总结

协程其实模拟了人类活动的一种过程。例如:你准备先写文档,而后修复bug。这时候接到电话说这个bug很严重,必须当即修复(能够看做CPU通知)。因而你暂停写文档,开始去填坑,终于你把坑填完了,你回来写文档,这个时候你确定是接着以前写的文档继续,难道你要把以前写的给删了,从新写?这就是协程。那若是是子例程呢?那你就必须从新写了,由于退出以后,栈帧就会被弹出销毁,再次调用就是开辟新的栈空间了。

总结:协程就是用户态下的线程,是人们在有了进程、线程以后仍以为效率不够,而追求的又一种高并发解决方案。为何说是用户态,是由于操做系统并不知道它的存在,它是由程序员本身控制、互相协做的让出控制权而不是像进程、线程那样由操做系统调度决定是否让出控制权。

相关文章
相关标签/搜索