從GIL開始重新認識Python多線程編程

我們想要了解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在某些情況下是可以被釋放掉的:

  1. GIL會根據執行的字節碼行數以及時間片進行釋放
  2. 程序會在遇到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))

在這裏插入圖片描述
這裏也可以看出,運行的時間並不是兩個線程的耗時相加

今天的內容就到這裏,下一篇文章將具體介紹多線程編程的應用案例。

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