併發和並行、同步和異步、多線程爬蟲

一、併發和並行

併發(concurrency)和並行( parallelism)是兩個相似的概念。 引用一個比較容易理解的說法,併發是指在一個時間段內發生若干事件的情況,並行是指在同一時刻發生若干事件的情況。

這個概念用單核CPU和多核CPU比較容易說明。在使用單核CPU時,多個工作任務是以併發的方式運行的,因爲只有一個CPU,所以各個任務會分別佔用CPU的一段時間依次執行。如果在自己分得的時間段沒有完成任務,就會切換到另一個任務,然後在下一次得到CPU使用權的時候再繼續執行,直到完成。在這種情況下,因爲各個任務的時間段很短、經常切換,所以給我們的感覺是“同時”進行。在使用多核CPU時,在各個核的任務能夠同時運行,這是真正的同時運行,也就是並行。

二、同步和異步

同步和異步也是兩個值得比較的概念。下面在併發和並行框架的基礎上理解同步和異步。

同步就是併發或並行的各個任務不是獨自運行的,任務之間有一定的交替順序,可能在運行完一個任務得到結果後,另一個任務纔會開始運行。就像接力賽跑一樣,要拿到交接棒之後下一個選手纔可以開始跑。

異步則是併發或並行的各個任務可以獨立運行,一個任務的運行不受另一個任務影響,任務之間就像比賽的各個選手在不同的賽道比賽一樣, 跑步的速度不受其他賽道選手的影響。

在網絡爬蟲中,假設你需要打開4個不同的網站,IO過程就相當於你打開網站的過程,CPU就是你單擊的動作。你單擊的動作很快,但是網站卻打開得很慢,同步IO是指你每單擊一個網址,要等待該網站徹底顯示纔可以單擊下一個網站。異步IO是指你單擊定一個網址,不用等對方服務器返回結果,立馬可以用新打開的測覽器窗口打開另一個網址,最後同時等待4個網站徹底打開。

很明顯,異步的速度要快得多。

三、多線程爬蟲

多線程爬蟲是以併發的方式執行的。也就是說,多個線程並不能真正的同時執行,而是通過進程的快速切換加快網絡爬蟲速度的。

Python本身的設計對多線程的執行有所限制。在Python設計之初,爲了數據安全所做的決定設置有GIL(Global Interpreter Lock,全局解釋器鎖)。在Python中,一個線程的執行過程包括獲取GIL、執行代碼直到掛起和釋放GIL。

例如,某個線程想要執行,必須先拿到GIL,我們可以把GIL看成“通行證”,並且在一個Python進程中,GIL只有一個。拿不到通行證的進程就不允許進入CPU執行。

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

由於GIL的存在,多線程是不是就沒用了呢?

以網絡爬蟲來說,網絡爬蟲是IO密集型,多線程能夠有效地提升效率,因爲單線程下有IO操作會進行IO等待,所以會造成不必要的時間浪費,而開啓多線程能在線程A等待時自動切換到線程B,可以不浪費CPU的資源,從而提升程序執行的效率。

Python的多線程對於IO密集型代碼比較友好,網絡爬蟲能夠在獲取網頁的過程中使用多線程,從而加快速度。

在這裏插入圖片描述

下面將以獲取訪問量最大的1000箇中文網站的速度爲例,通過和單線程的爬蟲比較,證實多線程方法的速度提升。

1000個網址寫在alexa.txt文件中。
在這裏插入圖片描述

3.1 簡單單線程爬蟲

import requests
import time

link_list = []
with open('alexa.txt', 'r') as file:
    file_list = file.readlines()
    for eachone in file_list:
        link = eachone.split('\t')[1]
        link = link.replace('\n','')
        link_list.append(link)
    
start = time.time()
for eachone in link_list:
    try:
        r = requests.get(eachone)
        print(r.status_code, eachone)
    except Exception as e:
        print("Error: ", e)
end = time.time()
print("串行的總時間爲:", end-start)

得到的時間結果大概是:2030.428秒

3.2 簡單的多線程爬蟲

import threading
import time
import requests

link_list = []
with open('alexa.txt', 'r') as file:
    file_list = file.readlines()
    for eachone in file_list:
        link = eachone.split('\t')[1]
        link = link.replace('\n','')
        link_list.append(link)

start = time.time()
class MyThread(threading.Thread):
    def __init__(self, name, link_range):
        threading.Thread.__init__(self)
        self.name = name
        self.link_range = link_range
    def run(self):
        print("Starting " + self.name)
        crawler(self.name, self.link_range)
        print("Exiting " + self.name)
        
def crawler(threadName, link_range):
    for i in range(link_range[0], link_range[1]+1):
        try:
            r = requests.get(link_list[i], timeout=20)
            print(threadName, r.status_code, link_list[i])
        except Exception as e:
            print(threadName, 'Error: ', e)

thread_list = []
link_range_list = [(0,200), (201,400), (401,600), (601,800), (801,1000)]

#創建新線程
for i in range(1,6):
    thread = MyThread("Thread-" + str(i), link_range_list[i-1])
    thread.start()
    thread_list.append(thread)

#等待所有線程完成
for thread in thread_list:
    thread.join()
    
end = time.time()
print("簡單多線程爬蟲總時間爲:", end-start)
print("Exiting Main Thread")

在這裏插入圖片描述
結果大概是532.81秒,明顯快了很多。

3.3 使用Queue的多線程爬蟲

這裏,我們不再分給每個線程分配固定個數個網址,因爲某個線程可能先於其他線程完成任務,然後就會空閒,這就造成了浪費。我們用一個隊列存儲所有網址,每個線程每次都從隊列裏取一個網址來執行。

import threading
import time
import requests
import queue as Queue

link_list = []
with open('alexa.txt', 'r') as file:
    file_list = file.readlines()
    for eachone in file_list:
        link = eachone.split('\t')[1]
        link = link.replace('\n','')
        link_list.append(link)

start = time.time()
class MyThread(threading.Thread):
    def __init__(self, name, q):
        threading.Thread.__init__(self)
        self.name = name
        self.q = q
    def run(self):
        print("Starting " + self.name)
        while True:
            try:
                crawler(self.name, self.q)
            except:
                break
        print("Exiting " + self.name)
        
def crawler(threadName, q):
    url = q.get(timeout=2)
    try:
        r = requests.get(url, timeout=20)
        print(q.qsize(), threadName, r.status_code, url)
    except Exception as e:
         print(q.qsize(), threadName, url, 'Error: ', e)

threadList = ["Thread-1", "Thread-2", "Thread-3", "Thread-4", "Thread-5"]
workQueue = Queue.Queue(1000)
threads = []

for url in link_list:
    workQueue.put(url)

#創建新線程
for tName in threadList:
    thread = MyThread(tName, workQueue)
    thread.start()
    threads.append(thread)

#等待所有線程完成
for thread in threads:
    thread.join()
    
end = time.time()
print("Queue多線程爬蟲總時間爲:", end-start)
print("Exiting Main Thread")

在這裏插入圖片描述
結果大概是441.40秒,又快了很多。

四、Python使用多線程的兩種方法

(1)函數式:調用_thread模塊中的start_new_thread()函數產生新線程:

import _thread
import time

#爲線程定義一個函數
def print_time(threadName, delay):
    count = 0
    while count < 3:
        time.sleep(delay)
        count += 1
        print(threadName, time.ctime())
        
_thread.start_new_thread(print_time, ("Thread-1", 1))
_thread.start_new_thread(print_time, ("Thread-2", 2))
print("Main Finished")

輸出結果如下:

Main Finished
Thread-1 Wed Apr 8 21:44:57 2020
Thread-2 Wed Apr 8 21:44:58 2020
Thread-1 Wed Apr 8 21:44:58 2020
Thread-1 Wed Apr 8 21:44:59 2020
Thread-2 Wed Apr 8 21:45:00 2020
Thread-2 Wed Apr 8 21:45:02 2020


(2)類包裝式:調用Threading庫創建線程,從threading.Thread繼承

  • run():用以表示線程活動的方法。
  • start():啓動線程活動。
  • join([time]):等待至線程至終止。阻塞調用線程直至線程的join()方法被調用爲止。
  • isAlive():返回線程是否是活動的。
  • getName():返回線程名。
  • setName():設置線程名。
import threading
import time

class MyThread(threading.Thread):
    def __init__(self, name, delay):
        threading.Thread.__init__(self)
        self.name = name
        self.delay = delay
    def run(self):
        print("Starting " + self.name)
        print_time(self.name, self.delay)
        print("Exiting " + self.name)

def print_time(threadingName, delay):
    counter = 0
    while counter < 3:
        time.sleep(delay)
        print(threadingName, time.ctime())
        counter += 1

threads = []

#創建新線程
thread1 = MyThread("Thread-1", 1)
thread2 = MyThread("Thread-2", 2)

#開啓新線程
thread1.start()
thread2.start()

#添加線程到線程列表
threads.append(thread1)
threads.append(thread2)

#等待所有線程完成
for t in threads:
    t.join()
    
print("Exiting Main Thread")

結果如下:
Starting Thread-1
Starting Thread-2
Thread-1 Wed Apr 8 21:53:14 2020
Thread-1 Wed Apr 8 21:53:15 2020
Thread-2 Wed Apr 8 21:53:15 2020
Thread-1 Wed Apr 8 21:53:16 2020
Exiting Thread-1
Thread-2 Wed Apr 8 21:53:17 2020
Thread-2 Wed Apr 8 21:53:19 2020
Exiting Thread-2
Exiting Main Thread

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章