python中多进程+协程的使用以及为什么要用它
前面講了為什么python里推薦用多進(jìn)程而不是多線程,但是多進(jìn)程也有其自己的限制:相比線程更加笨重、切換耗時更長,并且在python的多進(jìn)程下,進(jìn)程數(shù)量不推薦超過CPU核心數(shù)(一個進(jìn)程只有一個GIL,所以一個進(jìn)程只能跑滿一個CPU),因為一個進(jìn)程占用一個CPU時能充分利用機(jī)器的性能,但是進(jìn)程多了就會出現(xiàn)頻繁的進(jìn)程切換,反而得不償失。
不過特殊情況(特指IO密集型任務(wù))下,多線程是比多進(jìn)程好用的。
舉個例子:給你200W條url,需要你把每個url對應(yīng)的頁面抓取保存起來,這種時候,單單使用多進(jìn)程,效果肯定是很差的。為什么呢?
例如每次請求的等待時間是2秒,那么如下(忽略cpu計算時間):
1、單進(jìn)程+單線程:需要2秒*200W=400W秒==1111.11個小時==46.3天,這個速度明顯是不能接受的
2、單進(jìn)程+多線程:例如我們在這個進(jìn)程中開了10個多線程,比1中能夠提升10倍速度,也就是大約4.63天能夠完成200W條抓取,請注意,這里的實際執(zhí)行是:線程1遇見了阻塞,CPU切換到線程2去執(zhí)行,遇見阻塞又切換到線程3等等,10個線程都阻塞后,這個進(jìn)程就阻塞了,而直到某個線程阻塞完成后,這個進(jìn)程才能繼續(xù)執(zhí)行,所以速度上提升大約能到10倍(這里忽略了線程切換帶來的開銷,實際上的提升應(yīng)該是不能達(dá)到10倍的),但是需要考慮的是線程的切換也是有開銷的,所以不能無限的啟動多線程(開200W個線程肯定是不靠譜的)
3、多進(jìn)程+多線程:這里就厲害了,一般來說也有很多人用這個方法,多進(jìn)程下,每個進(jìn)程都能占一個cpu,而多線程從一定程度上繞過了阻塞的等待,所以比單進(jìn)程下的多線程又更好使了,例如我們開10個進(jìn)程,每個進(jìn)程里開20W個線程,執(zhí)行的速度理論上是比單進(jìn)程開200W個線程快10倍以上的(為什么是10倍以上而不是10倍,主要是cpu切換200W個線程的消耗肯定比切換20W個進(jìn)程大得多,考慮到這部分開銷,所以是10倍以上)。
還有更好的方法嗎?答案是肯定的,它就是:
4、協(xié)程,使用它之前我們先講講what/why/how(它是什么/為什么用它/怎么使用它)
what:
協(xié)程是一種用戶級的輕量級線程。協(xié)程擁有自己的寄存器上下文和棧。協(xié)程調(diào)度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復(fù)先前保存的寄存器上下文和棧。因此:
協(xié)程能保留上一次調(diào)用時的狀態(tài)(即所有局部狀態(tài)的一個特定組合),每次過程重入時,就相當(dāng)于進(jìn)入上一次調(diào)用的狀態(tài),換種說法:進(jìn)入上一次離開時所處邏輯流的位置。
在并發(fā)編程中,協(xié)程與線程類似,每個協(xié)程表示一個執(zhí)行單元,有自己的本地數(shù)據(jù),與其它協(xié)程共享全局?jǐn)?shù)據(jù)和其它資源。
why:
目前主流語言基本上都選擇了多線程作為并發(fā)設(shè)施,與線程相關(guān)的概念是搶占式多任務(wù)(Preemptive multitasking),而與協(xié)程相關(guān)的是協(xié)作式多任務(wù)。
不管是進(jìn)程還是線程,每次阻塞、切換都需要陷入系統(tǒng)調(diào)用(system call),先讓CPU跑操作系統(tǒng)的調(diào)度程序,然后再由調(diào)度程序決定該跑哪一個進(jìn)程(線程)。
而且由于搶占式調(diào)度執(zhí)行順序無法確定的特點,使用線程時需要非常小心地處理同步問題,而協(xié)程完全不存在這個問題(事件驅(qū)動和異步程序也有同樣的優(yōu)點)。
因為協(xié)程是用戶自己來編寫調(diào)度邏輯的,對CPU來說,協(xié)程其實是單線程,所以CPU不用去考慮怎么調(diào)度、切換上下文,這就省去了CPU的切換開銷,所以協(xié)程在一定程度上又好于多線程。
how:
python里面怎么使用協(xié)程?答案是使用gevent,使用方法:看這里
使用協(xié)程,可以不受線程開銷的限制,我嘗試過一次把20W條url放在單進(jìn)程的協(xié)程里執(zhí)行,完全沒問題。
所以最推薦的方法,是多進(jìn)程+協(xié)程(可以看作是每個進(jìn)程里都是單線程,而這個單線程是協(xié)程化的)
多進(jìn)程+協(xié)程下,避開了CPU切換的開銷,又能把多個CPU充分利用起來,這種方式對于數(shù)據(jù)量較大的爬蟲還有文件讀寫之類的效率提升是巨大的。
?
小例子:
#-*- coding=utf-8 -*-
import requests
from multiprocessing import Process
import gevent
from gevent import monkey; monkey.patch_all()
import sys
reload(sys)
sys.setdefaultencoding('utf8')
def fetch(url):
try:
s = requests.Session()
r = s.get(url,timeout=1)#在這里抓取頁面
except Exception,e:
print e
return ''
def process_start(url_list):
tasks = []
for url in url_list:
tasks.append(gevent.spawn(fetch,url))
gevent.joinall(tasks)#使用協(xié)程來執(zhí)行
def task_start(filepath,flag = 100000):#每10W條url啟動一個進(jìn)程
with open(filepath,'r') as reader:#從給定的文件中讀取url
url = reader.readline().strip()
url_list = []#這個list用于存放協(xié)程任務(wù)
i = 0 #計數(shù)器,記錄添加了多少個url到協(xié)程隊列
while url!='':
i += 1
url_list.append(url)#每次讀取出url,將url添加到隊列
if i == flag:#一定數(shù)量的url就啟動一個進(jìn)程并執(zhí)行
p = Process(target=process_start,args=(url_list,))
p.start()
url_list = [] #重置url隊列
i = 0 #重置計數(shù)器
url = reader.readline().strip()
if url_list not []:#若退出循環(huán)后任務(wù)隊列里還有url剩余
p = Process(target=process_start,args=(url_list,))#把剩余的url全都放到最后這個進(jìn)程來執(zhí)行
p.start()
if __name__ == '__main__':
task_start('./testData.txt')#讀取指定文件
細(xì)心的同學(xué)會發(fā)現(xiàn):上面的例子中隱藏了一個問題:進(jìn)程的數(shù)量會隨著url數(shù)量的增加而不斷增加,我們在這里不使用進(jìn)程池multiprocessing.Pool來控制進(jìn)程數(shù)量的原因是multiprocessing.Pool和gevent有沖突不能同時使用,但是有興趣的同學(xué)可以研究一下gevent.pool這個協(xié)程池。
---------------------
作者:L瑜
來源:CSDN
原文:https://blog.csdn.net/lambert310/article/details/51162634
版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請附上博文鏈接!
轉(zhuǎn)載于:https://www.cnblogs.com/ExMan/p/10426777.html
總結(jié)
以上是生活随笔為你收集整理的python中多进程+协程的使用以及为什么要用它的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2. Python3输入与输出
- 下一篇: utf-8 字符串转为Unicode编码