Python 爬蟲-多線程爬蟲


多線程:

什么是多線程:

  1. 理解:默認情況下,一個程序只有一個進程和一個線程,代碼是依次線性執行的。而多線程則可以並發執行,一次性多個人做多件事,自然比單線程更快。
  2. 官方:https://baike.baidu.com/item/多線程/1190404?fr=aladdin

如何創建一個基本的多線程:

使用threading模塊下的Thread類即可創建一個線程。這個類有一個target參數,需要指定一個函數,那么以后這個線程執行的時候,就會執行這個函數的代碼。示例代碼如下:

import time
import threading

def coding():
    for x in range(3):
        print("%d正在寫代碼..."%x)
        time.sleep(1)

def drawing():
    for x in range(3):
        print("%d正在畫圖..." % x)
        time.sleep(1)

def multi_thread():
    th1 = threading.Thread(target=coding)
    th2 = threading.Thread(target=drawing)

    th1.start()
    th2.start()

if __name__ == '__main__':
    # single_thread()
    multi_thread()

查看當前線程:

  1. threading.current_thread:在線程中執行這個函數,會返回當前線程的對象。
  2. threading.enumerate:獲取整個程序中所有的線程。

繼承自threading.Thread類:

  1. 我們自己寫的類必須繼承自threading.Thread類。
  2. 線程代碼需要放在run方法中執行。
  3. 以后創建線程的時候,直接使用我們自己創建的類來創建線程就可以了。
  4. 為什么要使用類的方式創建線程呢?原因是因為類可以更加方便的管理我們的代碼,可以讓我們使用面向對象的方式進行編程。

全局變量共享的問題:

在多線程中,如果需要修改全局變量,那么需要在修改全局變量的地方使用鎖鎖起來,執行完成后再把鎖釋放掉。
使用鎖的原則:

  1. 把盡量少的和不耗時的代碼放到鎖中執行。
  2. 代碼執行完成后要記得釋放鎖。
    在Python中,可以使用threading.Lock來創建鎖,lock.acquire()是上鎖操作,lock.release()是釋放鎖的操作。

生產者和消費者模式:

生產者和消費者模式是多線程開發中經常見到的一種模式。生產者的線程專門用來生產一些數據,然后存放到一個中間的變量中。消費者再從這個中間的變量中取出數據進行消費。通過生產者和消費者模式,可以讓代碼達到高內聚低耦合的目標,程序分工更加明確,線程更加方便管理。

Lock版本的生產者和消費者模式:

import threading
import random

gMoney = 0
gLock = threading.Lock()
gTimes = 0


class Producer(threading.Thread):
    def run(self) -> None:
        global gMoney
        global gTimes
        while True:
            gLock.acquire()
            if gTimes >= 10:
                gLock.release()
                break
            money = random.randint(0, 100)
            gMoney += money
            gTimes += 1
            print("%s生產了%d元錢"%(threading.current_thread().name,money))
            gLock.release()


class Consumer(threading.Thread):
    def run(self) -> None:
        global gMoney
        while True:
            gLock.acquire()
            money = random.randint(0,100)
            if gMoney >= money:
                gMoney -= money
                print("%s消費了%d元錢"%(threading.current_thread().name,money))
            else:
                if gTimes >= 10:
                    gLock.release()
                    break
                print("%s想消費%d元錢,但是余額只有%d"%(threading.current_thread().name,money,gMoney))
            gLock.release()

def main():
    for x in range(5):
        th = Producer(name="生產者%d號"%x)
        th.start()

    for x in range(5):
        th = Consumer(name="消費者%d號"%x)
        th.start()

if __name__ == '__main__':
    main()

Condition版本的生產者和消費者模式:

Lock版本的生產者與消費者模式可以正常的運行。但是存在一個不足,在消費者中,總是通過while True死循環並且上鎖的方式去判斷錢夠不夠。上鎖是一個很耗費CPU資源的行為。因此這種方式不是最好的。還有一種更好的方式便是使用threading.Condition來實現。threading.Condition可以在沒有數據的時候處於阻塞等待狀態。一旦有合適的數據了,還可以使用notify相關的函數來通知其他處於等待狀態的線程。這樣就可以不用做一些無用的上鎖和解鎖的操作。可以提高程序的性能。首先對threading.Condition相關的函數做個介紹,threading.Condition類似threading.Lock,可以在修改全局數據的時候進行上鎖,也可以在修改完畢后進行解鎖。以下將一些常用的函數做個簡單的介紹:

  1. acquire:上鎖。
  2. release:解鎖。
  3. wait:將當前線程處於等待狀態,並且會釋放鎖。可以被其他線程使用notify和notify_all函數喚醒。被喚醒后會繼續等待上鎖,上鎖后繼續執行下面的代碼。
  4. notify:通知某個正在等待的線程,默認是第1個等待的線程。
  5. notify_all:通知所有正在等待的線程。notify和notify_all不會釋放鎖。並且需要在release之前調用。

代碼如下:

import threading
import random
import time

gMoney = 0
gCondition = threading.Condition()
gTimes = 0


class Producer(threading.Thread):
    def run(self) -> None:
        global gMoney
        global gTimes
        while True:
            gCondition.acquire()
            if gTimes >= 10:
                gCondition.release()
                break
            money = random.randint(0, 100)
            gMoney += money
            gTimes += 1
            print("%s生產了%d元錢,剩余%d元錢"%(threading.current_thread().name,money,gMoney))
            gCondition.notify_all()
            gCondition.release()
            time.sleep(1)


class Consumer(threading.Thread):
    def run(self) -> None:
        global gMoney
        while True:
            gCondition.acquire()
            money = random.randint(0,100)
            while gMoney < money:
                if gTimes >= 10:
                    print("%s想消費%d元錢,但是余額只有%d元錢了,並且生產者已經不再生產了!"%(threading.current_thread().name,money,gMoney))
                    gCondition.release()
                    return
                print("%s想消費%d元錢,但是余額只有%d元錢了,消費失敗!"%(threading.current_thread().name,money,gMoney))
                gCondition.wait()
            gMoney -= money
            print("%s消費了%d元錢,剩余%d元錢"%(threading.current_thread().name,money,gMoney))
            gCondition.release()
            time.sleep(1)


def main():
    for x in range(5):
        th = Producer(name="生產者%d號"%x)
        th.start()

    for x in range(5):
        th = Consumer(name="消費者%d號"%x)
        th.start()

if __name__ == '__main__':
    main()

線程安全的隊列Queue:

在線程中,訪問一些全局變量,加鎖是一個經常的過程。如果你是想把一些數據存儲到某個隊列中,那么Python內置了一個線程安全的模塊叫做queue模塊。Python中的queue模塊中提供了同步的、線程安全的隊列類,包括FIFO(先進先出)隊列Queue,LIFO(后入先出)隊列LifoQueue。這些隊列都實現了鎖原語(可以理解為原子操作,即要么不做,要么都做完),能夠在多線程中直接使用。可以使用隊列來實現線程間的同步。相關的函數如下:
初始化Queue(maxsize):創建一個先進先出的隊列。

  1. qsize():返回隊列的大小。
  2. empty():判斷隊列是否為空。
  3. full():判斷隊列是否滿了。
  4. get():從隊列中取最后一個數據。默認情況下是阻塞的,也就是說如果隊列已經空了,那么再調用就會一直阻塞,直到有新的數據添加進來。也可以使用block=False,來關掉阻塞。如果關掉了阻塞,在隊列為空的情況獲取就會拋出異常。
  5. put():將一個數據放到隊列中。跟get一樣,在隊列滿了的時候也會一直阻塞,並且也可以通過block=False來關掉阻塞,同樣也會拋出異常。

GIL:

什么是GIL:

Python自帶的解釋器是CPython。CPython解釋器的多線程實際上是一個假的多線程(在多核CPU中,只能利用一核,不能利用多核)。同一時刻只有一個線程在執行,為了保證同一時刻只有一個線程在執行,在CPython解釋器中有一個東西叫做GIL(Global Intepreter Lock),叫做全局解釋器鎖。這個解釋器鎖是有必要的。因為CPython解釋器的內存管理不是線程安全的。當然除了CPython解釋器,還有其他的解釋器,有些解釋器是沒有GIL鎖的,見下面:

  1. Jython:用Java實現的Python解釋器。不存在GIL鎖。更多詳情請見:https://zh.wikipedia.org/wiki/Jython
  2. IronPython:用.net實現的Python解釋器。不存在GIL鎖。更多詳情請見:https://zh.wikipedia.org/wiki/IronPython
  3. PyPy:用Python實現的Python解釋器。存在GIL鎖。更多詳情請見:https://zh.wikipedia.org/wiki/PyPy
    GIL雖然是一個假的多線程。但是在處理一些IO操作(比如文件讀寫和網絡請求)還是可以在很大程度上提高效率的。在IO操作上建議使用多線程提高效率。在一些CPU計算操作上不建議使用多線程,而建議使用多進程。

有了GIL,為什么還需要Lock:

GIL只是保證全局同一時刻只有一個線程在執行,但是他並不能保證執行代碼的原子性。也就是說一個操作可能會被分成幾個部分完成,這樣就會導致數據有問題。所以需要使用Lock來保證操作的原子性。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM