Python 协程

进程是资源分配的最小单位,而线程则是cpu调度的最小单位。所以cpu最小只能调度一个线程,而对于一个线程内部的协程,cpu是无法进行切换的,即使遇到I/O阻塞。它只能通过程序内部代码自己来调度。

比如有10个任务,要求要并发的执行,我们有如下方法可以做到:

1. 开启多个进程并发执行,操作系统切换+保持状态

2.开启多线程并发执行,操作系统切换+保持状态

3.开启协程并发执行,程序自己控制cpu在三个任务间切换+保持状态。

 

使用协程的方式有一个缺点,假如三个任务同时遇到了阻塞,那么操作系统可能将cpu切换走。

但优点也很明显:

开销小,执行速度快,同时能一直占用cpu资源(操作系统认为一直在执行),当遇到I/O操作时能自动切换到其它协程。

从上面可以看出,并发的本质 就是 :在保存状态的情况下切换。

 

greenlet模块只能完成切换,但遇到I/O时无法切换

from greenlet import greenlet

def eat(name):
    print('%s eat 1' % name)
    g2.switch('andy')
    print('%s eat 2' % name)
    g2.switch()


def play(name):
    print('%s play 1' % name)
    g1.switch()
    print('%s play 2' % name)


g1.greenlet(eat)
g2.greenlet(play)

g1.switch('andy')

#输出
andy eat 1
andy play 1
andy eat 2
andy play 2

它的执行过程如下:

主线程的 g1.switch('andy'), 然后执行eat函数, 打印 andy eat 1, 接着切换到g2, 打印 andy play 1, 现次遇到切换 andy eat 2, 最后再次切换g2, 

打印andy play 2.

但我们的目的不仅仅是切换,而是在保存状态的切换的状态下,而且在遇到IO的情况下也能自动切换。

下面看它的改进版:

import gevent

def eat(name):
    print('%s eat 1' % name)
    gevent.sleep(2)
    print('%s eat 2' % name)


def play(name):
    print('%s play 1' % name)
    gevent.sleep(1)
    print('%s play 2' % name)


g1 = gevent.spawn(eat, 'andy')
g2 = gevent.spawn(play, 'jack')


g1.join()
g2.join()
#或者gevent.joinall([g1,g2])
print('主')

#输出:
andy eat 1
jack play 1
jack play 2
andy eat 2
主

这里有一点需要注意,假如我不写Join的话,那么上面的两个函数是不会执行的,这里只有一个主线程,主线程正常情况下会等待所有蜚守护线程的子线程执行完再结束 ,便这里没有其它子线程,主线程完了,也就整个执行完了,内部协程还没来得及执行(这里的理解 可能不正确)。joIn也是ducktype。

另外 这里使用的是gevent.sleep, 这是gevent能识别的阻塞,而如果换成time.sleep是无法识别的,也就无法切换。

所以就需要打补丁,即monkey.patch_all() 它的作用相当于对前代码下面的所有任务中的阻塞打上标记,遇到这种标记的的IO时就会切换。

下面是最终版本的代码

import time
import gevent
from gevent import monkey

monkey.patch_all()


def eat(name):
    print('%s eat 1' % name)
    time.sleep(2)
    print('%s eat 2' % name)


def play(name):
    print('%s play 1' % name)
    time.sleep(1)
    print('%s play 2' % name)


g1 = gevent.spawn(eat, 'andy')
g2 = gevent.spawn(play, 'jack')


# g1.join()
# g2.join()
gevent.joinall([g1,g2])
print('主')

#输出
andy eat 1
jack play 1
jack play 2
andy eat 2
主

 注意:from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前

 

一般在工作中我们都是进程+线程+协程的方式来实现并发,以达到最好的并发效果,如果是4核的cpu,一般起5个进程,每个进程中20个线程(5倍cpu数量),每个线程可以起500个协程,大规模爬取页面的时候,等待网络延迟的时间的时候,我们就可以用协程去实现并发。 并发数量 = 5 * 20 * 500 = 50000个并发,这是一般一个4cpu的机器最大的并发数。nginx在负载均衡的时候最大承载量就是5w个

 

上一篇:Python 进程池与线程池

下一篇:Python 闭包与装饰器