Python爬蟲進階五之多線程的用法

前言

咱們以前寫的爬蟲都是單個線程的?這怎麼夠?一旦一個地方卡到不動了,那不就永遠等待下去了?爲此咱們可使用多線程或者多進程來處理。html

首先聲明一點!python

多線程和多進程是不同的!一個是 thread 庫,一個是 multiprocessing 庫。而多線程 thread 在 Python 裏面被稱做雞肋的存在!而沒錯!本節介紹的是就是這個庫 thread。git

不建議你用這個,不過仍是介紹下了,若是想看能夠看看下面,不想浪費時間直接看github

multiprocessing 多進程python3.x

雞肋點

名言:

「Python下多線程是雞肋,推薦使用多進程!」安全

那固然有同窗會問了,爲啥?網絡

背景

一、GIL是什麼?多線程

GIL的全稱是Global Interpreter Lock(全局解釋器鎖),來源是python設計之初的考慮,爲了數據安全所作的決定。併發

二、每一個CPU在同一時間只能執行一個線程(在單核CPU下的多線程其實都只是併發,不是並行,併發和並行從宏觀上來說都是同時處理多路請求的概念。但併發和並行又有區別,並行是指兩個或者多個事件在同一時刻發生;而併發是指兩個或多個事件在同一時間間隔內發生。)app

在Python多線程下,每一個線程的執行方式:

  • 獲取GIL
  • 執行代碼直到sleep或者是python虛擬機將其掛起。
  • 釋放GIL

可見,某個線程想要執行,必須先拿到GIL,咱們能夠把GIL看做是「通行證」,而且在一個python進程中,GIL只有一個。拿不到通行證的線程,就不容許進入CPU執行。

在Python2.x裏,GIL的釋放邏輯是當前線程碰見IO操做或者ticks計數達到100(ticks能夠看做是Python自身的一個計數器,專門作用於GIL,每次釋放後歸零,這個計數能夠經過 sys.setcheckinterval 來調整),進行釋放。

而每次釋放GIL鎖,線程進行鎖競爭、切換線程,會消耗資源。而且因爲GIL鎖存在,python裏一個進程永遠只能同時執行一個線程(拿到GIL的線程才能執行),這就是爲何在多核CPU上,python的多線程效率並不高。

那麼是否是python的多線程就徹底沒用了呢?

在這裏咱們進行分類討論:

一、CPU密集型代碼(各類循環處理、計數等等),在這種狀況下,因爲計算工做多,ticks計數很快就會達到閾值,而後觸發GIL的釋放與再競爭(多個線程來回切換固然是須要消耗資源的),因此python下的多線程對CPU密集型代碼並不友好。

二、IO密集型代碼(文件處理、網絡爬蟲等),多線程可以有效提高效率(單線程下有IO操做會進行IO等待,形成沒必要要的時間浪費,而開啓多線程能在線程A等待時,自動切換到線程B,能夠不浪費CPU的資源,從而能提高程序執行效率)。因此python的多線程對IO密集型代碼比較友好。

而在python3.x中,GIL不使用ticks計數,改成使用計時器(執行時間達到閾值後,當前線程釋放GIL),這樣對CPU密集型程序更加友好,但依然沒有解決GIL致使的同一時間只能執行一個線程的問題,因此效率依然不盡如人意。

多核性能

多核多線程比單核多線程更差,緣由是單核下多線程,每次釋放GIL,喚醒的那個線程都能獲取到GIL鎖,因此可以無縫執行,但多核下,CPU0釋放GIL後,其餘CPU上的線程都會進行競爭,但GIL可能會立刻又被CPU0拿到,致使其餘幾個CPU上被喚醒後的線程會醒着等待到切換時間後又進入待調度狀態,這樣會形成線程顛簸(thrashing),致使效率更低

多進程爲何不會這樣?

每一個進程有各自獨立的GIL,互不干擾,這樣就能夠真正意義上的並行執行,因此在python中,多進程的執行效率優於多線程(僅僅針對多核CPU而言)。

因此在這裏說結論:多核下,想作並行提高效率,比較通用的方法是使用多進程,可以有效提升執行效率。

因此,若是不想浪費時間,能夠直接看多進程。

直接利用函數建立多線程

Python中使用線程有兩種方式:函數或者用類來包裝線程對象。

函數式:調用thread模塊中的start_new_thread()函數來產生新線程。語法以下:

 

 

參數說明:

  • function – 線程函數。
  • args – 傳遞給線程函數的參數,他必須是個tuple類型。
  • kwargs – 可選參數。

先用一個實例感覺一下:

 

 

運行結果以下:

 

 

能夠發現,兩個線程都在執行,睡眠2秒和4秒後打印輸出一段話。

注意到,在主線程寫了

 

 

這是讓主線程一直在等待

若是去掉上面兩行,那就直接輸出

 

 

程序執行結束。

使用Threading模塊建立線程

使用Threading模塊建立線程,直接從threading.Thread繼承,而後重寫init方法和run方法:

 

 

運行結果:

 

 

有沒有發現什麼奇怪的地方?打印的輸出格式好奇怪。好比第一行以後應該是一個回車的,結果第二個進程就打印出來了。

那是由於什麼?由於這幾個線程沒有設置同步。

線程同步

若是多個線程共同對某個數據修改,則可能出現不可預料的結果,爲了保證數據的正確性,須要對多個線程進行同步。

使用Thread對象的Lock和Rlock能夠實現簡單的線程同步,這兩個對象都有acquire方法和release方法,對於那些須要每次只容許一個線程操做的數據,能夠將其操做放到acquire和release方法之間。以下:

多線程的優點在於能夠同時運行多個任務(至少感受起來是這樣)。可是當線程須要共享數據時,可能存在數據不一樣步的問題。

考慮這樣一種狀況:一個列表裏全部元素都是0,線程」set」從後向前把全部元素改爲1,而線程」print」負責從前日後讀取列表並打印。

那麼,可能線程」set」開始改的時候,線程」print」便來打印列表了,輸出就成了一半0一半1,這就是數據的不一樣步。爲了不這種狀況,引入了鎖的概念。

鎖有兩種狀態——鎖定和未鎖定。每當一個線程好比」set」要訪問共享數據時,必須先得到鎖定;若是已經有別的線程好比」print」得到鎖定了,那麼就讓線程」set」暫停,也就是同步阻塞;等到線程」print」訪問完畢,釋放鎖之後,再讓線程」set」繼續。

通過這樣的處理,打印列表時要麼所有輸出0,要麼所有輸出1,不會再出現一半0一半1的尷尬場面。

看下面的例子:

 

 

在上面的代碼中運用了線程鎖還有join等待。

運行結果以下:

 

 

這樣一來,你能夠發現就不會出現剛纔的輸出混亂的結果了。

線程優先級隊列

Python的Queue模塊中提供了同步的、線程安全的隊列類,包括FIFO(先入先出)隊列Queue,LIFO(後入先出)隊列LifoQueue,和優先級隊列PriorityQueue。這些隊列都實現了鎖原語,可以在多線程中直接使用。可使用隊列來實現線程間的同步。

Queue模塊中的經常使用方法:

  • Queue.qsize() 返回隊列的大小
  • Queue.empty() 若是隊列爲空,返回True,反之False
  • Queue.full() 若是隊列滿了,返回True,反之False
  • Queue.full 與 maxsize 大小對應
  • Queue.get([block[, timeout]])獲取隊列,timeout等待時間
  • Queue.get_nowait() 至關Queue.get(False)
  • Queue.put(item) 寫入隊列,timeout等待時間
  • Queue.put_nowait(item) 至關Queue.put(item, False)
  • Queue.task_done() 在完成一項工做以後,Queue.task_done()函數向任務已經完成的隊列發送一個信號
  • Queue.join() 實際上意味着等到隊列爲空,再執行別的操做

用一個實例感覺一下:

 

 

運行結果:

 

 

上面的例子用了FIFO隊列。固然你也能夠換成其餘類型的隊列。

參考文章

  1. http://bbs.51cto.com/thread-1349105-1.html

2. http://www.runoob.com/python/python-multithreading.html

轉載:靜覓 » Python爬蟲進階五之多線程的用法

相關文章
相關標籤/搜索