python协程示例

来源:互联网 发布:木地板衬垫淘宝 编辑:程序博客网 时间:2024/05/18 03:47

前面讲了为什么python里推荐用多进程而不是多线程,但是多进程也有其自己的限制:相比线程更加笨重、切换耗时更长,并且在python的多进程下,进程数量不推荐超过CPU核心数(一个进程只有一个GIL,所以一个进程只能跑满一个CPU),因为一个进程占用一个CPU时能充分利用机器的性能,但是进程多了就会出现频繁的进程切换,反而得不偿失。

不过特殊情况(特指IO密集型任务)下,多线程是比多进程好用的。

举个例子:给你200W条url,需要你把每个url对应的页面抓取保存起来,这种时候,单单使用多进程,效果肯定是很差的。为什么呢?

例如每次请求的等待时间是2秒,那么如下(忽略cpu计算时间):

1、单进程+单线程:需要2秒*200W=400W秒==1111.11个小时==46.3天,这个速度明显是不能接受的

2、单进程+多线程:例如我们在这个进程中开了10个多线程,比1中能够提升10倍速度,也就是大约4.63天能够完成200W条抓取,请注意,这里的实际执行是:线程1遇见了阻塞,CPU切换到线程2去执行,遇见阻塞又切换到线程3等等,10个线程都阻塞后,这个进程就阻塞了,而直到某个线程阻塞完成后,这个进程才能继续执行,所以速度上提升大约能到10倍(这里忽略了线程切换带来的开销,实际上的提升应该是不能达到10倍的),但是需要考虑的是线程的切换也是有开销的,所以不能无限的启动多线程(开200W个线程肯定是不靠谱的)

3、多进程+多线程:这里就厉害了,一般来说也有很多人用这个方法,多进程下,每个进程都能占一个cpu,而多线程从一定程度上绕过了阻塞的等待,所以比单进程下的多线程又更好使了,例如我们开10个进程,每个进程里开20W个线程,执行的速度理论上是比单进程开200W个线程快的(为什么不是相同,主要是cpu切换200W个线程的消耗肯定比切换20W个进程大得多,考虑到这部分开销)。

还有更好的方法吗?答案是肯定的,它就是:

4、协程,使用它之前我们先讲讲what/why/how(它是什么/为什么用它/怎么使用它)

what:

协程是一种用户级的轻量级线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:

协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其它协程共享全局数据和其它资源。

why:

目前主流语言基本上都选择了多线程作为并发设施,与线程相关的概念是抢占式多任务(Preemptive multitasking),而与协程相关的是协作式多任务。

不管是进程还是线程,每次阻塞、切换都需要陷入系统调用(system call),先让CPU跑操作系统的调度程序,然后再由调度程序决定该跑哪一个进程(线程)。
而且由于抢占式调度执行顺序无法确定的特点,使用线程时需要非常小心地处理同步问题,而协程完全不存在这个问题(事件驱动和异步程序也有同样的优点)。

因为协程是用户自己来编写调度逻辑的,对CPU来说,协程其实是单线程,所以CPU不用去考虑怎么调度、切换上下文,这就省去了CPU的切换开销,所以协程在一定程度上又好于多线程。

how:

python里面怎么使用协程?答案是使用gevent,使用方法:

Python通过yield提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。

gevent是第三方库,通过greenlet实现协程,其基本思想是:

当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。

由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成:

from gevent import monkey; monkey.patch_socket()import geventdef f(n):    for i in range(n):        print gevent.getcurrent(), ig1 = gevent.spawn(f, 5)g2 = gevent.spawn(f, 5)g3 = gevent.spawn(f, 5)g1.join()g2.join()g3.join()

运行结果:

<Greenlet at 0x10e49f550: f(5)> 0<Greenlet at 0x10e49f550: f(5)> 1<Greenlet at 0x10e49f550: f(5)> 2<Greenlet at 0x10e49f550: f(5)> 3<Greenlet at 0x10e49f550: f(5)> 4<Greenlet at 0x10e49f910: f(5)> 0<Greenlet at 0x10e49f910: f(5)> 1<Greenlet at 0x10e49f910: f(5)> 2<Greenlet at 0x10e49f910: f(5)> 3<Greenlet at 0x10e49f910: f(5)> 4<Greenlet at 0x10e49f4b0: f(5)> 0<Greenlet at 0x10e49f4b0: f(5)> 1<Greenlet at 0x10e49f4b0: f(5)> 2<Greenlet at 0x10e49f4b0: f(5)> 3<Greenlet at 0x10e49f4b0: f(5)> 4

可以看到,3个greenlet是依次运行而不是交替运行。

要让greenlet交替运行,可以通过gevent.sleep()交出控制权:

def f(n):    for i in range(n):        print gevent.getcurrent(), i        gevent.sleep(0)

执行结果:

<Greenlet at 0x10cd58550: f(5)> 0<Greenlet at 0x10cd58910: f(5)> 0<Greenlet at 0x10cd584b0: f(5)> 0<Greenlet at 0x10cd58550: f(5)> 1<Greenlet at 0x10cd584b0: f(5)> 1<Greenlet at 0x10cd58910: f(5)> 1<Greenlet at 0x10cd58550: f(5)> 2<Greenlet at 0x10cd58910: f(5)> 2<Greenlet at 0x10cd584b0: f(5)> 2<Greenlet at 0x10cd58550: f(5)> 3<Greenlet at 0x10cd584b0: f(5)> 3<Greenlet at 0x10cd58910: f(5)> 3<Greenlet at 0x10cd58550: f(5)> 4<Greenlet at 0x10cd58910: f(5)> 4<Greenlet at 0x10cd584b0: f(5)> 4

3个greenlet交替运行,

把循环次数改为500000,让它们的运行时间长一点,然后在操作系统的进程管理器中看,线程数只有1个。

当然,实际代码里,我们不会用gevent.sleep()去切换协程,而是在执行到IO操作时,gevent自动切换,代码如下:

from gevent import monkey; monkey.patch_all()import geventimport urllib2def f_url(url):    print "GET:" + url    resp = urllib2.urlopen(url)    data = resp.read()    print "%d by recived from %s" %(len(data), url)    gevent.sleep(0)gevent.joinall(    [gevent.spawn(f_url,"https://segmentfault.com"),     gevent.spawn(f_url,"https://www.python.org"),     gevent.spawn(f_url,"https://www.github.com")])

运行结果:

GET:https://segmentfault.comGET:https://www.python.orgGET:https://www.github.com48879 by recived from https://www.python.org201988 by recived from https://segmentfault.com51476 by recived from https://www.github.comProcess finished with exit code 0

从结果看,3个网络操作是并发执行的,而且结束顺序不同,但只有一个线程。

小结:

使用gevent,可以获得极高的并发性能,但gevent只能在Unix/Linux下运行,在Windows下不保证正常安装和运行。

由于gevent是基于IO切换的协程,所以最神奇的是,我们编写的Web App代码,不需要引入gevent的包,也不需要改任何代码,仅仅在部署的时候,用一个支持gevent的WSGI服务器,立刻就获得了数倍的性能提升。具体部署方式可以参考后续“实战”-“部署Web App”一节。


使用协程,可以不受线程开销的限制,我尝试过一次把20W条url放在单进程的协程里执行,完全没问题。

所以最推荐的方法,是多进程+协程(可以看作是每个进程里都是单线程,而这个单线程是协程化的)

多进程+协程下,避开了CPU切换的开销,又能把多个CPU充分利用起来,这种方式对于数据量较大的爬虫还有文件读写之类的效率提升是巨大的。


小例子:

[python] view plain copy
  1. #-*- coding=utf-8 -*-  
  2. import requests  
  3. from multiprocessing import Process  
  4. import gevent  
  5. from gevent import monkey; monkey.patch_all()  
  6.   
  7. import sys  
  8. reload(sys)  
  9. sys.setdefaultencoding('utf8')  
  10. def fetch(url):  
  11.     try:  
  12.         s = requests.Session()  
  13.         r = s.get(url,timeout=1)#在这里抓取页面  
  14.     except Exception,e:  
  15.         print e   
  16.     return ''  
  17.    
  18. def process_start(url_list):  
  19.     tasks = []  
  20.     for url in url_list:  
  21.         tasks.append(gevent.spawn(fetch,url))  
  22.     gevent.joinall(tasks)#使用协程来执行  
  23.   
  24. def task_start(filepath,flag = 100000):#每10W条url启动一个进程  
  25.     with open(filepath,'r') as reader:#从给定的文件中读取url  
  26.         url = reader.readline().strip()  
  27.         url_list = []#这个list用于存放协程任务  
  28.         i = 0 #计数器,记录添加了多少个url到协程队列  
  29.         while url!='':  
  30.             i += 1  
  31.             url_list.append(url)#每次读取出url,将url添加到队列  
  32.             if i == flag:#一定数量的url就启动一个进程并执行  
  33.                 p = Process(target=process_start,args=(url_list,))  
  34.                 p.start()  
  35.                 url_list = [] #重置url队列  
  36.                 i = 0 #重置计数器  
  37.             url = reader.readline().strip()  
  38.         if url_list not []:#若退出循环后任务队列里还有url剩余  
  39.             p = Process(target=process_start,args=(url_list,))#把剩余的url全都放到最后这个进程来执行  
  40.             p.start()  
  41.     
  42. if __name__ == '__main__':  
  43.     task_start('./testData.txt')#读取指定文件  
  44.    


细心的同学会发现:上面的例子中隐藏了一个问题:进程的数量会随着url数量的增加而不断增加,我们可以先统计一下cpu核数来确定线程数量,再根据testData.txt里面的行数取平均值。我们在这里不使用进程池multiprocessing.Pool来控制进程数量的原因是multiprocessing.Pool和gevent有冲突不能同时使用,但是有兴趣的同学可以研究一下gevent.pool这个协程池。

原文:http://blog.csdn.net/lambert310/article/details/51162634

原创粉丝点击