八、學習分佈式爬蟲之多線程

多線程爬蟲

  1. 理解多線程
  2. 掌握threading模塊的使用
  3. 掌握生產者消費者模式
  4. 理解GIL
  5. 能用多線程寫爬蟲
    什麼是多線程
    理解:默認情況下,一個程序只有一個進程和一個線程,代碼是依次線性執行的,而多線程則可以併發執行,一次性多個人做多件事,自然比單線程更快。
    在這裏插入圖片描述
    threading模塊
    threading模塊是python中專門提供用來做多線程編程的模塊。threading模塊中最常用的類是Thread。
import time
import threading

def conding():
    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 single_thread():  #單線程花費6秒
    start = time.time()
    conding()
    drawing()
    end = time.time()
    used_time = end-start
    return used_time

def mulei_thread(): #多線程執行花費3秒
    t1 = threading.Thread(target=conding)
    t2 = threading.Thread(target=drawing)
    t1.start()
    t2.start()

if __name__ == '__main__':
    # use = single_thread()
    mulei_thread()

繼承自threading.Thread類
首先來了解兩個小知識點:

  1. 使用threading.current_thread()可以看到當前線程的信息
  2. 使用threading.enumerate()函數便可以看到當前的線程
    爲了讓線程代碼更好的封裝,可以使用threading模塊下的Thread類,繼承自這個類,然後實現run方法,線程就會自動運行run方法中的代碼
import time
import threading
#
# def conding():
#     the_thread = threading.current_thread() #查看當前線程信息
#     print(the_thread.name) #線程名字
#     for x in range(3):
#         print('%s正在寫代碼'%the_thread.name)
#         time.sleep(1)
#
# def drawing():
#     the_thread = threading.current_thread()
#     for x in range(3):
#         print('%s正在畫畫'%the_thread.name)
#         time.sleep(1)
#
#
# def mulei_thread():
#     t1 = threading.Thread(target=conding,name='th1')
#     t2 = threading.Thread(target=drawing,name='th2')
#     t1.start()
#     t2.start()
#     print(threading.enumerate()) #[<_MainThread(MainThread, started 10328)>, <Thread(th1, started 16888)>, <Thread(th2, started 19120)>]
#
# if __name__ == '__main__':
#     mulei_thread()

#=================繼承自threading.Thread類=================================
#1.我們自己寫的類必須繼承自‘threading.Thread’類
#2.線程代碼需要放在run方法中執行
#3.以後創建線程的時候,直接使用我們自己創建的類來創建線程就可以了
#4.爲什麼要使用類的方式創建線程呢?原因是因爲類可以更加方便的管理我們的代碼,可以讓我們使用面向對象的方式進行編程
class codingThread(threading.Thread):
    def run(self):
        the_thread = threading.current_thread()
        for x in range(3):
            print('%s正在寫代碼'%the_thread.name)
            time.sleep(1)

class drawingThread(threading.Thread):
    def run(self):
        the_thread = threading.current_thread()
        for x in range(3):
            print('%s正在畫畫'%the_thread.name)
            time.sleep(1)

def multi_thread():
    th1 = codingThread()
    th2 = drawingThread()

    th1.start()
    th2.start()

if __name__ == '__main__':
    multi_thread()

多線程共享全局變量問題
多線程都是在同一個進程中運行的,因此在進程中的全局變量所有線程都是可共享的,這就造成了一個問題,因爲線程執行是無序的,有可能造成數據錯誤。比如以下代碼,正常結果本應該是1000000和2000000,但是因爲多線程運行的不確定性,因此最後結果可能是隨機的。

import threading
value = 0

def add_value():
    #如果在函數中修改了全局變量,那麼需要使用global關鍵字進行聲明
    global value
    for x in range(1000000):
        value += 1
    print('value的值%s'%value)

def main():
    for x in range(2):
        th = threading.Thread(target=add_value)
        th.start()

if __name__ == '__main__':
    main()

爲了解決以上問題,就要用到鎖的機制
鎖機制和threading.Lock類
爲了解決共享全局變量的問題,threading提供了一個Lock類,這個類可以在某個線程訪問某個變量的時候加鎖,其他線程此時就不能進來,直到當前線程處理完後,把鎖釋放了,其他線程才能進來。
使用鎖的原則:

  1. 把儘量少的和不耗時的代碼放到鎖中執行
  2. 代碼執行完成後要記得釋放鎖
import threading

value = 0

gLock = threading.Lock()

def add_value():
    #如果在函數中修改了全局變量,那麼需要使用global關鍵字進行聲明
    global value
    gLock.acquire() #上鎖,當其他線程來到這裏時,發現已被上鎖,就在這裏進行等待
    for x in range(1000000):
        value += 1
    gLock.release() #釋放鎖
    print('value的值%s'%value)

def main():
    for x in range(2):
        th = threading.Thread(target=add_value)
        th.start()

if __name__ == '__main__':
    main()

注意:在多線程中,如果需要修改全局變量,那麼需要在修改全局變量的地方使用鎖鎖起來,執行完成後把鎖釋放掉
生產者消費者模式(包子鋪)
生產者消費者模式時多線程開發中經常見到的一種模式,生產者的線程專門用來生產一些數據,然後放到一箇中間的變量中,消費者再從這個中間的變量中取出數據進行消費。通過生產者消費者模式,可以讓代碼達到高內聚低耦合的目標,程序分工更加明確,線程更加方便管理。
高內聚:當前的代碼不會對外面的代碼產生影響或依賴。
低耦合:多個模塊之間影響比較小
在這裏插入圖片描述
放到爬蟲上來說就是:將爬取數據的代碼放到一個線程當中(生產者),爬取下來的數據存儲在一箇中間容器中,然後將保存數據的代碼放在另外一個線程中(消費者),將存放在中間容器中的數據取出來保存在硬盤中。
Lock版本的生產者消費者模式

import threading
import random

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

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

class Consumer(threading.Thread):
    def run(self):
        global gMoney
        global gTime
        while True:
            gLock.acquire()
            money = random.randint(0,100)
            if gMoney >= money:
                gMoney -= money
                print("%s消費了%d元錢"%(threading.current_thread().name,money))
            else:
                if gTime >= 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:通知某個正在等待的線程,默認時第一個等待的線程
  5. notify_all:通知所有等待的線程。notify和notify_all不會釋放鎖,並且需要在release之前調用
import threading
import random
import time

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

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

class Consumer(threading.Thread):
    def run(self):
        global gMoney
        global gTime
        while True:
            gCondition.acquire()
            money = random.randint(0,100)
            while gMoney < money:
                if gTime >= 10:
                    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。這些隊列都實現了鎖原語(可以理解爲原子操作,即要麼不做,要麼做完),能夠在多線程中直接使用。可以使用隊列來實現線程間的同步。相關的函數如下:

  1. Queue(size):創建一個先進先出的隊列
  2. qsize():返回隊列的大小
  3. empty():判斷隊列是否爲空,返回布爾值
  4. full():判斷隊列是否滿了,返回布爾值
  5. get():從隊列中取最後一個數據,默認情況下時阻塞的,也就是說如果隊列空了,那麼再調用就會一直阻塞,知道有新的數據添加進來,也可以使用block=False來關掉阻塞,在隊列爲空的情況獲取就會拋出異常
  6. put():將一個數據放到隊列中,跟get一樣,在隊列滿了的時候也會一直堵塞,並且可以通過block=False來關掉阻塞,同樣也會拋出異常
from queue import Queue
import random
import threading
import time

# q = Queue(4)  #最多能存儲四個數據
#
# for x in range(4):
#     q.put(x,block=False) #block=False當數據超出隊列最大尺寸,不會阻塞,而是拋出異常
#
# if q.full():
#     print('滿了')
#
# print(q)  #<queue.Queue object at 0x02E00FB8>
# print(q.qsize()) #4
#
# for x in range(4):
#     value = q.get()
#     print(value)
#
# if q.empty():
#     print('空了')

def add_value(q):
    while True:
        ran = random.randint(0,10)
        q.put(ran)
        print('已經放入了:%d'%ran)
        time.sleep(1)

def get_value(q):
    while True:
        print("獲取到的值:%d"%q.get())

def main():
    q = Queue(10)
    th1 = threading.Thread(target=add_value,args=[q])
    th2 = threading.Thread(target=get_value,args=[q])

    th1.start()
    th2.start()

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