python爬蟲入門八:多程式/多執行緒

十八街發表於2019-01-07

什麼是多執行緒/多程式

引用蟲師的解釋:

計算機程式只不過是磁碟中可執行的,二進位制(或其它型別)的資料。它們只有在被讀取到記憶體中,被作業系統呼叫的時候才開始它們的生命期。

程式(有時被稱為重量級程式)是程式的一次執行。每個程式都有自己的地址空間,記憶體,資料棧以及其它記錄其執行軌跡的輔助資料。作業系統管理在其上執行的所有程式,併為這些程式公平地分配時間。

執行緒(有時被稱為輕量級程式)跟程式有些相似,不同的是,所有的執行緒執行在同一個程式中,共享相同的執行環境。我們可以想像成是在主程式或“主執行緒”中並行執行的“迷你程式”。

 

為什麼需要多執行緒/多程式

我們直接編寫的爬蟲程式是單執行緒的,在資料需求量不大時它能夠滿足我們的需求。

但如果資料量很大,比如要通過訪問數百數千個url去爬取資料,單執行緒必須等待當前url訪問完畢並且資料提取儲存完成後才可以對下一個url進行操作,一次只能對一個url進行操作;

我們使用多執行緒/多程式的話,就可以實現對多個url同時進行操作。這樣就能大大縮減了爬蟲執行時間。

 

實現多執行緒/多程式

多執行緒

python提供了兩組多執行緒介面,一是thread模組_thread,提供低等級介面;二是threading模組,在thread模組基礎上進行封裝,提供更容易使用的基於物件的介面,可以繼承Thread物件來實現多執行緒。

同時,還有其他執行緒相關的物件,如Timer、Lock等。

在這裡,我們使用threading模組實現多執行緒。

1. 新增執行緒

threading.Thread(target, args)

使用threading.Thread()新建一個執行緒,target是需要執行的函式,args是需要傳入該函式的引數,args接受一個tuple,即使只有一個引數也需要寫成(x,)形式

import threading

print(threading.active_count()) # 顯示當前啟用的執行緒數
print(threading.enumerate()) # 顯示當前啟用的執行緒
print(threading.current_thread()) # 當前執行的執行緒 


def thread_job():
    print(`This is a thread of %s` % threading.current_thread())

def main():
    thread = threading.Thread(target=thread_job,) # 新增一個執行緒
    thread.start() # 開始該執行緒

if __name__ == `__main__`:
    main()

2. 執行緒阻塞:join

join()的作用是呼叫該執行緒時,等待該執行緒完成後再繼續往下執行。

join通常用於主執行緒與子執行緒之間,主執行緒等待子執行緒執行完畢後再繼續執行,避免子程式和主程式同時執行,子程式還沒有執行完的時候主程式就已經執行結束。

import threading
import time

# 定義一個fun,傳入執行緒
def T1_job():
    print(`T1 start
`)
    for i in range(10):
        time.sleep(0.1)
    print(`T1 finish
`)
        
def T2_job():
    print(`T2 start
`)
    print(`T2 finish
`)
        
def main():
    thread1 = threading.Thread(target=T1_job, name=`T1`) # 新增執行緒,準備執行thread_job,命名T1
    thread2 = threading.Thread(target=T2_job, name=`T2`)
    
    thread1.start() # 執行該執行緒,沒有新增join的時候,同步執行main和thread_job
    thread2.start()
    
    thread1.join() # 等待thread1完成後才進行下一步-主程式
    thread2.join() # 等待thread2完成後才進行下一步-主程式
    print(`all done`)

if __name__ == `__main__`:
    main()

3. 資訊傳遞:Queue佇列

Queue是python標準庫中的執行緒安全的佇列(FIFO)實現,提供了一個適用於多執行緒程式設計的先進先出的資料結構,即佇列。

Queue是一種先進先出的資料結構,一般來說讀資料都從Queue頭讀,寫資料都從Queue尾寫入。

import threading
from queue import Queue

def job(l, q):
    for i in range(len(l)):
        l[i] = l[i]**2
    q.put(l) # 執行緒中,return獲取的值無法提取,需要放入q中

def multithreading():
    q = Queue() # 佇列
    threads = [] # 全部執行緒
    data = [[1, 2, 3], [3, 4, 5], [4,4,4], [5,5,5]]
    for i in range(4):
        # 4個執行緒來執行job函式
        t = threading.Thread(target=job, args=(data[i], q))
        t.start()
        threads.append(t) # 當前執行緒加入全部執行緒中
        
    # 對主執行緒中的每一個執行緒都執行join()
    for thread in threads:
        thread.join()
   
    results = [] # 儲存結果
    for _ in range(4):
        results.append(q.get()) # 從q中拿出值,每次只能按順序拿出一個值
    print(results)
    
if __name__ == `__main__`:
    multithreading()

# [[1, 4, 9], [9, 16, 25], [16, 16, 16], [25, 25, 25]]

4. 執行緒鎖:Lock

lock在不同執行緒使用同一共享記憶體時,能夠確保執行緒之間互不影響。

使用lock的方法是:在每個執行緒執行運算修改共享記憶體之前執行lock.acquire()將共享記憶體上鎖, 確保當前執行緒執行時,記憶體不會被其他執行緒訪問;

執行運算完畢後使用lock.release()將鎖開啟, 保證其他的執行緒可以使用該共享記憶體。

lock.acquire()和lock.release()必須成對出現

# lock鎖,當前執行緒執行完成後才進行下一程式
import threading

def job1():
    global A, lock
    lock.acquire() # 開啟鎖
    for i in range(10):
        A += 1
        time.sleep(0.2)
        print(`job1`, A)
    lock.release() # 關閉鎖
    
def job2():
    global A, lock
    lock.acquire() # 開啟鎖
    for i in range(10):
        A += 10
        time.sleep(0.2)
        print(`job2`, A)
    lock.release() # 關閉鎖
    
if __name__ == `__main__`:
    lock = threading.Lock() # lock鎖
    A = 0
    t1 = threading.Thread(target=job1)
    t2 = threading.Thread(target=job2)
    t1.start()
    t2.start()

將上述程式碼中的lock.acquire()和lock.release()四行程式碼註釋後執行,就是不加鎖的情況,這時候輸出結果都是混亂的。而加鎖後,輸出結果正常。

5. 執行緒池

執行緒池有幾種方法可以實現,這裡我們使用multiprocessing.dummy庫。

from multiprocessing.dummy import Pool as ThreadPool # 執行緒池
import threading

def job(i):
    print(i, `
`, threading.current_thread())
    
if __name__ == `__main__`:
    pool = ThreadPool(4) # 建立一個包含4個執行緒的執行緒池
    pool.map(job, range(12))
    pool.close() # 關閉執行緒池的寫入
    pool.join() # 阻塞,保證子執行緒執行完畢後再繼續主程式 

 

多程式

多程式multiprocessing和多執行緒threading類似,都是用在python中進行平行計算的,而多程式則是為了彌補python在多執行緒中的劣勢而出現的。

multiprocessing是使用計算機的多核進行運算,它可以避免多執行緒中GIL的影響。

python使用multiprocessing模組實現多程式,用法和threading基本一致。

1. 新增程式

multiprocessing.Process(target, args)

使用multiprocessing.Process新建一個程式,target是需要執行的函式,args是需要傳入該函式的引數,args接受一個tuple,即使只有一個引數也需要寫成(x,)形式

import multiprocessing as mp

def job(a,d):
    print(`aaaaa`)

if __name__==`__main__`:
    p1 = mp.Process(target=job,args=(1,2)) # 新增一個程式
    p1.start()
    p1.join()

2. 資訊傳遞:Queue佇列

多程式中的Queue使用同多執行緒一致,同樣為先進先出

多程式可以直接從multiprocessing.Queue()匯入Queue佇列。

import multiprocessing as mp

def job(q):
    res=0
    for i in range(1000):
        res+=i+i**2+i**3
    q.put(res)    # 將值放入佇列

if __name__==`__main__`:
    q = mp.Queue() # Queue佇列
    p1 = mp.Process(target=job,args=(q,))
    p2 = mp.Process(target=job,args=(q,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    res1 = q.get() # 從佇列中取出值
    res2 = q.get() # 從佇列中取出值
    print(res1, res2)

3. 程式池

import multiprocessing as mp

def job(x):
    return x*x

def multicore():
    pool = mp.Pool() # 定義一個程式池
    res = pool.map(job, range(100))
    print(res)

if __name__==`__main__`:
    multicore()

 關於程式池的更多資訊請跳轉至:

 4. 共享記憶體

一般的變數在程式之間是沒法進行通訊的,multiprocessing 給我們提供了 Value 和 Array 模組,他們可以在不通的程式中共同使用。

Value()和Array()都接受兩個引數,第一個為資料型別,第二個是傳入的數。

Value()可以接受傳入單個數值,Array()可以接受傳入一個一維陣列

import multiprocessing as mp

value1 = mp.Value(`i`, 0) # value接受單個數值,i表示一個帶符號的整型
array = mp.Array(`i`, [1, 2, 3, 4]) # Array接受一個一維陣列

array2 = mp.Array(`i`, [[1,2], [2,3]]) # 傳入一個二維陣列錯誤,傳入引數非一維陣列

資料型別如下:

Type code
C Type Python Type Minimum size in bytes
`b` signed char int 1
`B` unsigned char int 1
`u` py_UNICODE Unicode character 2
`h` signed short int 2
`H` unsigned short int 2
`i` signed int int 2
`I` unsigned int int 2
`l` signed long int 4
`L` unsigned long int 4
`q` signed long long int 8
`Q` unsigned long long int 8
`f` float float 4
`d`
double
float 
8

5. 程式鎖

程式鎖同執行緒鎖使用方法一致,lock在不同程式使用同一共享記憶體時,能夠確保程式之間互不影響。

使用lock的方法是:在每個程式執行運算修改共享記憶體之前執行lock.acquire()將共享記憶體上鎖, 確保當前程式執行時,記憶體不會被其他程式訪問;

執行運算完畢後使用lock.release()將鎖開啟, 保證其他的程式可以使用該共享記憶體。

lock.acquire()和lock.release()必須成對出現。 

import multiprocessing as mp

def job(v, num, l):
    l.acquire() # 鎖住
    for _ in range(5):
        time.sleep(0.1) 
        v.value += num # 獲取共享記憶體
        print(v.value)
    l.release() # 釋放

def multicore():
    l = mp.Lock() # 定義一個程式鎖
    v = mp.Value(`i`, 0) # 定義共享記憶體
    p1 = mp.Process(target=job, args=(v,1,l)) # 需要將lock傳入
    p2 = mp.Process(target=job, args=(v,3,l)) 
    p1.start()
    p2.start()
    p1.join()
    p2.join()

if __name__ == `__main__`:
    multicore()

 

如何選擇多執行緒/多程式

1. 結論

CPU密集型程式碼(各種迴圈處理、計算等等):使用多程式

IO密集型程式碼(檔案處理、網路爬蟲等):使用多執行緒

2. 解釋

多執行緒和多程式的理解可以類比於公路。

假設當前公路均為單行道,並且出於安全考慮,一個車道只能同時行駛一輛汽車,一條公路只有一名駕駛員。只有一名指揮者進行集中排程,駕駛員獲取到了指揮者的排程資訊才會駕駛。

單執行緒是隻有一條公路而且是單車道,只能同時行駛一輛汽車;

多執行緒是隻有一條公路,但是是多車道,可以同時行駛多輛汽車;

多程式是有很多條公路,每條公路可能是單車道也可能是多車道,同樣可以同時行駛多輛汽車。

 

因為GIL的存在,python中的多執行緒其實在同一時間只能執行一個執行緒,就像一名駕駛員只能同時駕駛一輛汽車。四執行緒類比於一條四車道的公路,但是駕駛員可以從駕駛車道A上的汽車切換至駕駛車道B上的汽車,駕駛員切換的速度夠快的話,看起來就像是這條公路上的四輛汽車都在同時行駛。指揮者釋出的命令只需要跨越車道就能傳遞給駕駛員,命令傳輸的時間損耗相對較小。所以對於多執行緒,我們希望指揮者可以比較頻繁釋出命令,駕駛員獲取到命令後能夠很快就完成然後切換到下一個車道繼續執行命令,這樣看起來就像是駕駛員同時駕駛四輛汽車了。所以對於IO密集型程式碼,推薦使用多執行緒。

而對於多程式來說,每條公路都有一名駕駛員,四執行緒類比於四條公路,則四名駕駛員可以同時駕駛四輛汽車。但指揮者釋出的命令需要跨越公路才能傳遞給駕駛員,命令傳輸的時間損耗相對較大。所以對於多程式,我們希望指揮者釋出一次命令後駕駛員可以執行較長時間,這樣就不必把時間過多花費在資訊傳輸上。所以對於CPU密集型程式碼,推薦使用多程式。 

 

參考資料

1. python的多執行緒中的join的作用

2. python佇列Queue

3. Python多執行緒(2)——執行緒同步機制

4. 莫煩PYTHON-Threading多執行緒

5. Python 多程式鎖 多程式共享記憶體

6. python學習筆記——多程式中共享記憶體Value & Array

7. 莫煩PYTHON-multiprocessing多程式

8. python 之 多程式

9. Python多程式 

10. Python 使用multiprocessing 特別耗記憶體

11. 廖雪峰-程式和執行緒 

12. python 多執行緒,詳細教程,執行緒同步,執行緒加鎖,ThreadPoolExecutor

13. 多程式 multiprocessing 多執行緒Threading 執行緒池和程式池concurrent.futures

相關文章