咱們以前寫的爬蟲都是單個線程的?這怎麼夠?一旦一個地方卡到不動了,那不就永遠等待下去了?爲此咱們可使用多線程或者多進程來處理。html
首先聲明一點!python
多線程和多進程是不同的!一個是 thread 庫,一個是 multiprocessing 庫。而多線程 thread 在 Python 裏面被稱做雞肋的存在!而沒錯!本節介紹的是就是這個庫 thread。git
不建議你用這個,不過仍是介紹下了,若是想看能夠看看下面,不想浪費時間直接看github
multiprocessing 多進程python3.x
「Python下多線程是雞肋,推薦使用多進程!」安全
那固然有同窗會問了,爲啥?網絡
一、GIL是什麼?多線程
GIL的全稱是Global Interpreter Lock(全局解釋器鎖),來源是python設計之初的考慮,爲了數據安全所作的決定。併發
二、每一個CPU在同一時間只能執行一個線程(在單核CPU下的多線程其實都只是併發,不是並行,併發和並行從宏觀上來說都是同時處理多路請求的概念。但併發和並行又有區別,並行是指兩個或者多個事件在同一時刻發生;而併發是指兩個或多個事件在同一時間間隔內發生。)app
在Python多線程下,每一個線程的執行方式:
可見,某個線程想要執行,必須先拿到GIL,咱們能夠把GIL看做是「通行證」,而且在一個python進程中,GIL只有一個。拿不到通行證的線程,就不容許進入CPU執行。
在Python2.x裏,GIL的釋放邏輯是當前線程碰見IO操做或者ticks計數達到100(ticks能夠看做是Python自身的一個計數器,專門作用於GIL,每次釋放後歸零,這個計數能夠經過 sys.setcheckinterval 來調整),進行釋放。
而每次釋放GIL鎖,線程進行鎖競爭、切換線程,會消耗資源。而且因爲GIL鎖存在,python裏一個進程永遠只能同時執行一個線程(拿到GIL的線程才能執行),這就是爲何在多核CPU上,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()函數來產生新線程。語法以下:
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>
|
參數說明:
先用一個實例感覺一下:
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.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/python
# -*- coding: UTF-8 -*-
import threading
import time
import thread
exitFlag = 0
class myThread (threading.Thread): #繼承父類threading.Thread
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self): #把要執行的代碼寫到run函數裏面 線程在建立後會直接運行run函數
print "Starting " + self.name
print_time(self.name, self.counter, 5)
print "Exiting " + self.name
def print_time(threadName, delay, counter):
while counter:
if exitFlag:
thread.exit()
time.sleep(delay)
print "%s: %s" % (threadName, time.ctime(time.time()))
counter -= 1
# 建立新線程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 開啓線程
thread1.start()
thread2.start()
print "Exiting Main Thread"
|
運行結果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
Starting Thread-1Starting Thread-2
Exiting Main Thread
Thread-1: Thu Nov 3 18:42:19 2016
Thread-2: Thu Nov 3 18:42:20 2016
Thread-1: Thu Nov 3 18:42:20 2016
Thread-1: Thu Nov 3 18:42:21 2016
Thread-2: Thu Nov 3 18:42:22 2016
Thread-1: Thu Nov 3 18:42:22 2016
Thread-1: Thu Nov 3 18:42:23 2016
Exiting Thread-1
Thread-2: Thu Nov 3 18:42:24 2016
Thread-2: Thu Nov 3 18:42:26 2016
Thread-2: Thu Nov 3 18:42:28 2016
Exiting Thread-2
|
有沒有發現什麼奇怪的地方?打印的輸出格式好奇怪。好比第一行以後應該是一個回車的,結果第二個進程就打印出來了。
那是由於什麼?由於這幾個線程沒有設置同步。
若是多個線程共同對某個數據修改,則可能出現不可預料的結果,爲了保證數據的正確性,須要對多個線程進行同步。
使用Thread對象的Lock和Rlock能夠實現簡單的線程同步,這兩個對象都有acquire方法和release方法,對於那些須要每次只容許一個線程操做的數據,能夠將其操做放到acquire和release方法之間。以下:
多線程的優點在於能夠同時運行多個任務(至少感受起來是這樣)。可是當線程須要共享數據時,可能存在數據不一樣步的問題。
考慮這樣一種狀況:一個列表裏全部元素都是0,線程」set」從後向前把全部元素改爲1,而線程」print」負責從前日後讀取列表並打印。
那麼,可能線程」set」開始改的時候,線程」print」便來打印列表了,輸出就成了一半0一半1,這就是數據的不一樣步。爲了不這種狀況,引入了鎖的概念。
鎖有兩種狀態——鎖定和未鎖定。每當一個線程好比」set」要訪問共享數據時,必須先得到鎖定;若是已經有別的線程好比」print」得到鎖定了,那麼就讓線程」set」暫停,也就是同步阻塞;等到線程」print」訪問完畢,釋放鎖之後,再讓線程」set」繼續。
通過這樣的處理,打印列表時要麼所有輸出0,要麼所有輸出1,不會再出現一半0一半1的尷尬場面。
看下面的例子:
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
39
40
41
42
43
44
45
46
47
|
# -*- coding: UTF-8 -*-
import threading
import time
class myThread (threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print "Starting " + self.name
# 得到鎖,成功得到鎖定後返回True
# 可選的timeout參數不填時將一直阻塞直到得到鎖定
# 不然超時後將返回False
threadLock.acquire()
print_time(self.name, self.counter, 3)
# 釋放鎖
threadLock.release()
def print_time(threadName, delay, counter):
while counter:
time.sleep(delay)
print "%s: %s" % (threadName, time.ctime(time.time()))
counter -= 1
threadLock = threading.Lock()
threads = []
# 建立新線程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 開啓新線程
thread1.start()
thread2.start()
# 添加線程到線程列表
threads.append(thread1)
threads.append(thread2)
# 等待全部線程完成
for t in threads:
t.join()
print "Exiting Main Thread"
|
在上面的代碼中運用了線程鎖還有join等待。
運行結果以下:
1
2
3
4
5
6
7
8
9
|
Starting Thread-1
Starting Thread-2
Thread-1: Thu Nov 3 18:56:49 2016
Thread-1: Thu Nov 3 18:56:50 2016
Thread-1: Thu Nov 3 18:56:51 2016
Thread-2: Thu Nov 3 18:56:53 2016
Thread-2: Thu Nov 3 18:56:55 2016
Thread-2: Thu Nov 3 18:56:57 2016
Exiting Main Thread
|
這樣一來,你能夠發現就不會出現剛纔的輸出混亂的結果了。
Python的Queue模塊中提供了同步的、線程安全的隊列類,包括FIFO(先入先出)隊列Queue,LIFO(後入先出)隊列LifoQueue,和優先級隊列PriorityQueue。這些隊列都實現了鎖原語,可以在多線程中直接使用。可使用隊列來實現線程間的同步。
Queue模塊中的經常使用方法:
用一個實例感覺一下:
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
# -*- coding: UTF-8 -*-
import Queue
import threading
import time
exitFlag = 0
class myThread (threading.Thread):
def __init__(self, threadID, name, q):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.q = q
def run(self):
print "Starting " + self.name
process_data(self.name, self.q)
print "Exiting " + self.name
def process_data(threadName, q):
while not exitFlag:
queueLock.acquire()
if not workQueue.empty():
data = q.get()
queueLock.release()
print "%s processing %s" % (threadName, data)
else:
queueLock.release()
time.sleep(1)
threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = Queue.Queue(10)
threads = []
threadID = 1
# 建立新線程
for tName in threadList:
thread = myThread(threadID, tName, workQueue)
thread.start()
threads.append(thread)
threadID += 1
# 填充隊列
queueLock.acquire()
for word in nameList:
workQueue.put(word)
queueLock.release()
# 等待隊列清空
while not workQueue.empty():
pass
# 通知線程是時候退出
exitFlag = 1
# 等待全部線程完成
for t in threads:
t.join()
print "Exiting Main Thread"
|
運行結果:
1
2
3
4
5
6
7
8
9
10
11
12
|
Starting Thread-1
Starting Thread-2
Starting Thread-3
Thread-3 processing One
Thread-1 processing Two
Thread-2 processing Three
Thread-3 processing Four
Thread-2 processing Five
Exiting Thread-2
Exiting Thread-3
Exiting Thread-1
Exiting Main Thread
|
上面的例子用了FIFO隊列。固然你也能夠換成其餘類型的隊列。
2. http://www.runoob.com/python/python-multithreading.html
轉載:靜覓 » Python爬蟲進階五之多線程的用法