本次推文介紹一下多線程。不過值得注意的是,不能濫用多線程,多線程爬蟲請求內容速度過快,可能會導致服務器過載,或者是IP被封禁。為了避免這一問題,我們在使用多線程爬蟲的時候需要設置一個delay時間,用于請求同一域名時的最小時間間隔。
線程和進程如何工作
當程序在運行時,就會創建包含代碼和狀態的進程。這些進程通過一個或者多個CPU來執行。不過同一時刻每個CPU只會執行一個進程,然后在不同進程之間快速切換,這樣就感覺多個程序同時運行。同理,在一個進程中,程序的執行也是在不同線程間進行切換的,每個線程執行程序的不同部分。這就意味著一個線程在等待執行時,進程會切換到其他的線程執行,這樣可以避免浪費CPU時間。
Threading線程模塊
在Python標準庫中,使用threading模塊來支持多線程。Threading模塊對thread進行了封裝,絕大數情況,只需要使用threading這個模塊。使用起來也非常簡單:
t1=threading.Thread(target=run,args=("t1",)) 創建一個線程實例
# target是要執行的函數名(不是函數),args是函數對應的參數,以元組的形式存在
t1.start() 啟動這個線程實例。
普通創建方式
線程的創建很簡單,如下:
import threading
import time
def printStr(name):
print(name+"-python知識學堂")
s=0.5
time.sleep(s)
print(name+"-python知識學堂")
t1=threading.Thread(target=printStr,args=("你好!",))
t2=threading.Thread(target=printStr,args=("歡迎你!",))
t1.start()
t2.start()
自定義線程
本質是繼承threading.Thread,重構Thread類中的run方法
import threading
import time
class testThread(threading.Thread):
def __init__(self,s):
super(testThread,self).__init__()
self.s=s
def run(self):
print(self.s+"——python")
time.sleep(0.5)
print(self.s+"——知識學堂")
if __name__=='__main__':
t1=testThread("測試1")
t2=testThread("測試2")
t1.start()
t2.start()
守護線程
使用setDaemon(True)把子線程都變成主線程的守護線程,因此當主線程結束后,子線程也會隨之結束。也就是說,主線程不等待其守護線程執行完成再去關閉。
import threading
import time
def run(s):
print(s,"python")
time.sleep(0.5)
print(s,"知識學堂")
if name == "main":
t=threading.Thread(target=run,args=("你好!",))
t.setDaemon(True)
t.start()
print("end")
結果:
你好! python
end
當主線程結束后,守護線程不管有沒有結束,都自動結束。
主線程等待子線程結束
使用join方法,讓主線程等待子線程執行。如下:
import threading
import time
def run(s):
print(s,"python")
time.sleep(0.5)
print(s,"知識學堂")
if name == "main":
t=threading.Thread(target=run,args=("你好!",))
t.setDaemon(True)
t.start()
t.join()
print("end")
結果:
你好! python
你好! 知識學堂
end
以上是多線程的幾種簡單的用法,那么threading模塊還有做什么呢?請往下看。
Lock 鎖
其實在介紹diskcache緩存的時候也介紹過鎖的相關內容,其實不難理解為啥多線程中也會出現鎖的概念,當沒有保護共享資源時,多個線程在處理同一資源時,可能會出現臟數據,造成不可以預期的結果,即線程不安全。
如下示例出現不可預期的結果:
import threading
price=0
def changePrice(n):
global price
price=price+n
price=price-n
def runChange(n):
for i in range(2000000):
changePrice(n)
if name == "main":
t1=threading.Thread(target=runChange,args=(5,))
t2=threading.Thread(target=runChange,args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(price)
理論上的結果為0,但是每次運行的結果可能都是不一樣的。
所以這個時候就需要鎖去處理了,如下:
import threading
import time
from threading import Lock
price=0
def changePrice(n):
global price
lock.acquire() #獲取鎖
price=price+n
print("price:"+str(price))
price=price-n
lock.release() #釋放鎖
def runChange(n):
for i in range(2000000):
changePrice(n)
if name == "main":
lock=Lock()
t1=threading.Thread(target=runChange,args=(5,))
t2=threading.Thread(target=runChange,args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(price)
結果值與理論值是一致的。鎖的意義在于每次只允許一個線程去修改同一數據,以保證線程安全。
信號量
BoundedSemaphore類,同時允許一定數量的線程更改數據,如下:
import threading
import time
def work(n):
semaphore.acquire()
print("序號:"+str(n))
time.sleep(1)
semaphore.release()
if name == "main":
semaphore=threading.BoundedSemaphore(5)
for i in range(100):
t=threading.Thread(target=work,args=(i+1,))
t.start()
active_count獲取當前正在運行的線程數
while threading.active_count()!=1:
pass
else:
print("end")
結果為:每5次打印停頓一下,直到結束。
GIL全局解釋器鎖
說到多線程,不得不提一下GIL。GIL的全稱是Global Interpreter Lock(全局解釋器鎖),這是python設計之初,為了數據安全所做的決定。某個線程想要執行,必須先拿到GIL,并且在一個進程中,GIL只有一個。只有拿到GIL的線程,才能進入CPU執行。GIL只在cpython中才有,因為cpython調用的是c語言的原生線程,所以他不能直接操作cpu,只能利用GIL保證同一時間只能有一個線程拿到數據。而在pypy和jpython中是沒有GIL的。
總結
本篇文章介紹了多線程的用法,要根據實際的情況去使用。多線程編程,容易發生沖突,必須用鎖加以隔離,又得小心發生死鎖。由于Python的設計時有GIL全局鎖,導致多線程無法利用多核,使得在多線程并發的情況下并不理想。