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

官方描述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网络库。我们先理解这句,也是这次学习的重点——协程

wiki描述协程

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

没看懂?没关系,我也没看懂,不过算是有点线索:子例程

子例程

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

说到函数,我就想吐槽了,不明白为什么要叫函数。很多时候我们写一个函数是为了封装、模块化某个功能,它是一个功能、或者说是一个过程。因为它包含的是类似于流程图那样的具体逻辑,先怎样做,然后怎样做;如果遇到A情况则怎样,如果遇到B情况又怎样。个人觉得还是叫过程比较好,叫做函数就让人很纠结了,难道因为回归到底层还是计算问题,出于数学的角度把它称为函数?这个略坑啊!为了符合大家的口味,我还是称之为函数好了(其实我也习惯叫函数了%>_

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

123456789101112131415161718192021222324 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 startb startc startc endb enda end 

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

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

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

升级版

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

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

12345678 def producer():    print \”生产一个\”    consumer() def consumer():    print \”消费一个\”    producer() 

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

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

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

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

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

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

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

看下协程是怎样实现的:

12345678910111213141516171819202122232425262728293031 import randomfrom time import sleepfrom greenlet import greenletfrom Queue import Queue queue = Queue(1) @greenletdef 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() @greenletdef 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通知)。于是你暂停写文档,开始去填坑,终于你把坑填完了,你回来写文档,这个时候你肯定是接着之前写的文档继续,难道你要把之前写的给删了,重新写?这就是协程。那如果是子例程呢?那你就必须重新写了,因为退出之后,栈帧就会被弹出销毁,再次调用就是开辟新的栈空间了。

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