1. 程式人生 > >Python爬蟲之多執行緒,多程序

Python爬蟲之多執行緒,多程序

前言

我們之前寫的爬蟲都是單個執行緒的?這怎麼夠?一旦一個地方卡到不動了,那不就永遠等待下去了?為此我們可以使用多執行緒或者多程序來處理。

首先宣告一點!

多執行緒和多程序是不一樣的!一個是 thread 庫,一個是 multiprocessing 庫。而多執行緒 thread 在 Python 裡面被稱作雞肋的存在!而沒錯!本節介紹的是就是這個庫 thread。

不建議你用這個,不過還是介紹下了,如果想看可以看看下面,不想浪費時間直接看

multiprocessing 多程序

雞肋點

名言:

“Python下多執行緒是雞肋,推薦使用多程序!”

那當然有同學會問了,為啥?

背景

1、GIL是什麼?

GIL的全稱是Global Interpreter Lock(全域性直譯器鎖),來源是python設計之初的考慮,為了資料安全所做的決定。

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

在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的多執行緒就完全沒用了呢?

在這裡我們進行分類討論:

1、CPU密集型程式碼(各種迴圈處理、計數等等),在這種情況下,由於計算工作多,ticks計數很快就會達到閾值,然後觸發GIL的釋放與再競爭(多個執行緒來回切換當然是需要消耗資源的),所以python下的多執行緒對CPU密集型程式碼並不友好。

2、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()函式來產生新執行緒。語法如下:

 

 
1 <span class="s1">thread</span><span class="s2">.</span><span class="s1">start_new_thread </span><span class="s2">(</span> <span class="s3">function</span><span class="s2">,</span><span class="s1"> args</span><span class="s2">[,</span><span class="s1"> kwargs</span><span class="s2">]</span> <span class="s2">)</span>

 

引數說明:

  • function – 執行緒函式。
  • args – 傳遞給執行緒函式的引數,他必須是個tuple型別。
  • kwargs – 可選引數。

先用一個例項感受一下:

 

 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # -*- coding: UTF-8 -*-   import thread import time     # 為執行緒定義一個函式 def print_time(threadName, delay):     count = 0     while count < 5:         time.sleep(delay)         count += 1         print "%s: %s" % (threadName, time.ctime(time.time()))     # 建立兩個執行緒 try:     thread.start_new_thread(print_time, ("Thread-1", 2,))     thread.start_new_thread(print_time, ("Thread-2", 4,)) except:     print "Error: unable to start thread"     while 1:    pass   print "Main Finished"

 

執行結果如下:

 

 
1 2 3 4 5 6 7 8 9 10 Thread-1: Thu Nov  3 16:43:01 2016 Thread-2: Thu Nov  3 16:43:03 2016 Thread-1: Thu Nov  3 16:43:03 2016 Thread-1: Thu Nov  3 16:43:05 2016 Thread-2: Thu Nov  3 16:43:07 2016 Thread-1: Thu Nov  3 16:43:07 2016 Thread-1: Thu Nov  3 16:43:09 2016 Thread-2: Thu Nov  3 16:43:11 2016 Thread-2: Thu Nov  3 16:43:15 2016 Thread-2: Thu Nov  3 16:43:19 2016

 

可以發現,兩個執行緒都在執行,睡眠2秒和4秒後列印輸出一段話。

注意到,在主執行緒寫了

 

 
1 2 while 1:    pass

 

這是讓主執行緒一直在等待

如果去掉上面兩行,那就直接輸出

 

 
1 Main Finished

 

程式執行結束。

使用Threading模組建立執行緒

使用Threading模組建立執行緒,直接從threading.Thread繼承,然後重寫init方法和run方法:

 

 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #!/usr/bin/py