Python 爬虫进阶五之多线程的用法
我們之前寫的爬蟲都是單個線程的?這怎么夠?一旦一個地方卡到不動了,那不就永遠等待下去了?為此我們可以使用多線程或者多進程來處理。 首先聲明一點! 多線程和多進程是不一樣的!一個是 thread 庫,一個是 multiprocessing 庫。而多線程 thread 在 Python 里面被稱作雞肋的存在!而沒錯!本節介紹的是就是這個庫 thread。 不建議你用這個,不過還是介紹下了,如果想看可以看看下面,不想浪費時間直接看 multiprocessing 多進程
雞肋點
背景
在 Python 多線程下,每個線程的執行方式:
- 獲取 GIL
- 執行代碼直到 sleep 或者是 python 虛擬機將其掛起。
- 釋放 GIL
可見,某個線程想要執行,必須先拿到 GIL,我們可以把 GIL 看作是 “通行證”,并且在一個 python 進程中,GIL 只有一個。拿不到通行證的線程,就不允許進入 CPU 執行。 在 Python2.x 里,GIL 的釋放邏輯是當前線程遇見 IO 操作或者 ticks 計數達到 100(ticks 可以看作是 Python 自身的一個計數器,專門做用于 GIL,每次釋放后歸零,這個計數可以通過 sys.setcheckinterval 來調整),進行釋放。 而每次釋放 GIL 鎖,線程進行鎖競爭、切換線程,會消耗資源。并且由于 GIL 鎖存在,python 里一個進程永遠只能同時執行一個線程 (拿到 GIL 的線程才能執行),這就是為什么在多核 CPU 上,python 的多線程效率并不高。
那么是不是 python 的多線程就完全沒用了呢?
在這里我們進行分類討論:
而在 python3.x 中,GIL 不使用 ticks 計數,改為使用計時器(執行時間達到閾值后,當前線程釋放 GIL),這樣對 CPU 密集型程序更加友好,但依然沒有解決 GIL 導致的同一時間只能執行一個線程的問題,所以效率依然不盡如人意。
多核性能
多核多線程比單核多線程更差,原因是單核下多線程,每次釋放 GIL,喚醒的那個線程都能獲取到 GIL 鎖,所以能夠無縫執行,但多核下,CPU0 釋放 GIL 后,其他 CPU 上的線程都會進行競爭,但 GIL 可能會馬上又被 CPU0 拿到,導致其他幾個 CPU 上被喚醒后的線程會醒著等待到切換時間后又進入待調度狀態,這樣會造成線程顛簸 (thrashing),導致效率更低
多進程為什么不會這樣?
每個進程有各自獨立的 GIL,互不干擾,這樣就可以真正意義上的并行執行,所以在 python 中,多進程的執行效率優于多線程 (僅僅針對多核 CPU 而言)。 所以在這里說結論:多核下,想做并行提升效率,比較通用的方法是使用多進程,能夠有效提高執行效率。 所以,如果不想浪費時間,可以直接看多進程。
線程模塊
Python通過兩個標準庫thread和threading提供對線程的支持。thread提供了低級別的、原始的線程以及一個簡單的鎖。
threading 模塊提供的其他方法:
- threading.currentThread(): 返回當前的線程變量。
- threading.enumerate(): 返回一個包含正在運行的線程的list。正在運行指線程啟動后、結束前,不包括啟動前和終止后的線程。
- threading.activeCount(): 返回正在運行的線程數量,與len(threading.enumerate())有相同的結果。
除了使用方法外,線程模塊同樣提供了Thread類來處理線程,
Thread類提供了以下方法:
- run(): 用以表示線程活動的方法。
- start():啟動線程活動。
- join([time]): 等待至線程中止。這阻塞調用線程直至線程的join() 方法被調用中止-正常退出或者拋出未處理的異常-或者是可選的超時發生。
- isAlive(): 返回線程是否活動的。
- getName(): 返回線程名。
- setName(): 設置線程名。
直接利用函數創建多線程
Python 中使用線程有兩種方式:函數或者用類來包裝線程對象。
函數式:調用 thread 模塊中的 start_new_thread () 函數來產生新線程。語法如下:
thread.start_new_thread(function, args[, kwargs])參數說明:
- function - 線程函數。
- args - 傳遞給線程函數的參數,他必須是個 tuple 類型。
- kwargs - 可選參數。
先用一個實例感受一下:
# -*- coding: UTF-8 -*-import thread import time# 為線程定義一個函數 def print_time(threadName, delay):count = 0while count < 5:time.sleep(delay)count += 1print "%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" 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使用 Threading 模塊創建線程
使用 Threading 模塊創建線程,直接從 threading.Thread 繼承,然后重寫 init 方法和 run 方法:
import threading import time import threadexitFlag = 0 class myThread (threading.Thread): #繼承父類threading.Threaddef __init__(self, threadID, name, counter):threading.Thread.__init__(self)self.threadID = threadIDself.name = nameself.counter = counterdef run(self): #把要執行的代碼寫到run函數里面 線程在創建后會直接運行run函數print "Starting " + self.nameprint_time(self.name, self.counter, 5)print "Exiting " + self.namedef 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" Starting Thread-1 Starting Thread-2Exiting Main ThreadThread-1: Mon Jan 4 17:13:08 2021 Thread-2: Mon Jan 4 17:13:09 2021 Thread-1: Mon Jan 4 17:13:09 2021 Thread-1: Mon Jan 4 17:13:10 2021 Thread-2: Mon Jan 4 17:13:11 2021 Thread-1: Mon Jan 4 17:13:11 2021 Thread-1: Mon Jan 4 17:13:12 2021 Exiting Thread-1 Thread-2: Mon Jan 4 17:13:13 2021 Thread-2: Mon Jan 4 17:13:15 2021 Thread-2: Mon Jan 4 17:13:17 2021 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 的尷尬場面。 看下面的例子:
線程優先級隊列
Python 的 Queue 模塊中提供了同步的、線程安全的隊列類,包括 FIFO(先入先出) 隊列 Queue,LIFO(后入先出)隊列 LifoQueue,和優先級隊列 PriorityQueue。這些隊列都實現了鎖原語,能夠在多線程中直接使用。可以使用隊列來實現線程間的同步。
- Queue 模塊中的常用方法:
- Queue.qsize () 返回隊列的大小
- Queue.empty () 如果隊列為空,返回 True, 反之 False
- Queue.full () 如果隊列滿了,返回 True, 反之 False
- Queue.full 與 maxsize 大小對應
- Queue.get ([block [, timeout]]) 獲取隊列,timeout 等待時間
- Queue.get_nowait () 相當 Queue.get (False)
- Queue.put (item) 寫入隊列,timeout 等待時間
- Queue.put_nowait (item) 相當 Queue.put (item, False)
- Queue.task_done () 在完成一項工作之后,Queue.task_done () 函數向任務已經完成的隊列發送一個信號
- Queue.join () 實際上意味著等到隊列為空,再執行別的操作
總結
以上是生活随笔為你收集整理的Python 爬虫进阶五之多线程的用法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Word中如何设置图片与段落的间距为半行
- 下一篇: struct结构体初始化3种方法