我們想要了解Python得多線程,就必須要了解GIL,GIL即全局解釋鎖。
舉個栗子
計算機執行程序時,是需要把代碼編譯成機器指令再去執行的,我們現在用的編輯器,其實就是一種解釋器,在我們右鍵運行程序時,它能夠將整個文件編譯成字節碼,再由Python虛擬機來執行字節碼,最後得到輸出:
來看一下這個函數的字節碼:
Python中有多個線程在同一時間運行同一段代碼的時候呢,其實是很容易出錯的,所以Python語言在早期的時候爲了解決這一問題,便在解釋器里加了一個鎖,這個鎖能夠使得在同一時刻只有一個線程在CPU上面去執行這個字節碼。也就是說,同一時刻只能有一個線程在一個cpu上面執行字節碼。也正因如此,Python在執行多線程任務時,有人會覺得它慢。
這樣一來,無法顯示出多核cpu的優勢:
接下來我們看看有沒有什麼辦法能解決這個問題,Python有一個內置的模塊threading,它是專門用來解決多線程問題的:
import threading
a=0
def time():
global a #聲明全局變量
for item in range(1000000):
a+=1
def test():
global a
for item in range(1000000):
a -= 1
thread_1=threading.Thread(target=time)
thread_2=threading.Thread(target=test)
thread_1.start()
thread_2.start()
thread_1.join()
thread_2.join()
print(a)
按理來說,這個程序的輸出結果應該爲0,可是:
而且,每次運行時,結果都不一樣:
通過上面的運行結果,我們可以看出,整個線程並不是一旦佔有就一直佔有的!,就好像談戀愛,有可能會分手一樣,你以爲能走到最後,可結果…
因此,GIL在某些情況下是可以被釋放掉的:
- GIL會根據執行的字節碼行數以及時間片進行釋放
- 程序會在遇到IO操作的時候 ,會主動釋放 GIL
什麼是時間片?
比如說time這個線程,會被分配一個時間段來執行,這個時間段即時間片,也就是說,時間結束後,會進入到下一個線程,保證CPU資源不浪費
那麼我們怎麼進行多線程呢?
先來看看錯誤的寫法:
import time
import threading
def get_data_html():
print('開始獲取html數據的時間')
time.sleep(2)
print('獲取html數據結束的時間')
def get_data_url():
print('開始獲取url數據的時間')
time.sleep(2)
print('獲取url數據結束的時間')
if __name__ == '__main__':
thread_1=threading.Thread(target=get_data_html)
thread_2=threading.Thread(target=get_data_html)
start_time = time.time()
thread_1.start()
thread_2.start()
print("中間運行的時間:{}".format(time.time() - start_time))
按理說,程序應該執行2秒,可是並沒有,我們來debug一下:
其實這裏應該有三個線程,最後一個輸出語句是主線程,當主線程退出的時候,子線程會被kill掉了,因此線程沒有執行完畢,那麼我們可以模塊內置的功能去守護線程,讓線程繼續運行:
if __name__ == '__main__':
thread_1=threading.Thread(target=get_data_html)
thread_2=threading.Thread(target=get_data_url)
thread_1.setDaemon(True) #守護線程
thread_2.setDaemon(True)
start_time = time.time()
thread_1.start()
thread_2.start()
print("中間運行的時間:{}".format(time.time() - start_time))
可是問題又來了:
沒有完整地得到結果,我們試着關掉一個線程保護:
if __name__ == '__main__':
thread_1=threading.Thread(target=get_data_html)
thread_2=threading.Thread(target=get_data_url)
# thread_1.setDaemon(True)
thread_2.setDaemon(True)
start_time = time.time()
thread_1.start()
thread_2.start()
print("中間運行的時間:{}".format(time.time() - start_time))
爲什麼會出現這樣的輸出?
原因很簡單,守護了thread_2,那麼thread_2便不會自動結束,它將一直佔用CPU,導致thread_1結束時,thread_2還沒有結束
下面我們守護thread_1:
發現兩個線程都能輸出?這裏我們改一下time.sleep()的時間,我們讓thread_2的時間減少後再試一次:
總的來說,守護線程能避免當主線程退出的時候,子線程會被kill掉的情況
不過,即便如此,這樣的方式仍然不是我們想要的,我們希望在兩個線程都執行完以後,再來執行主線程
How to do it ?
線程阻塞能解決這個問題:
if __name__ == '__main__':
thread_1=threading.Thread(target=get_data_html)
thread_2=threading.Thread(target=get_data_url)
start_time = time.time()
thread_1.start()
thread_2.start()
thread_1.join()
thread_2.join()
print("中間運行的時間:{}".format(time.time() - start_time))
這裏也可以看出,運行的時間並不是兩個線程的耗時相加
今天的內容就到這裏,下一篇文章將具體介紹多線程編程的應用案例。