進程是系統進行資源分派最小單元,線程是進程的一個實體,是CPU調度和指派的基本單位,它是比進程更小的能獨立啟動並執行基本單位.進程在執行過程中擁有獨立的記憶體單元,而多個線程共用記憶體等資源。
threading模組建立線程
import threadingfrom threading import Threaddef test(x): print('this is {}'.format(x)) time.sleep(2)def get_thread(number=5): l_thread = (Thread(target=test, args=(i,)) for i in range(number)) for t in l_thread: print(t) t.start() # 啟動線程開始執行 print(len(threading.enumerate()))if __name__ == '__main__': get_thread(5)# 結果<Thread(Thread-1, initial)>this is 0<Thread(Thread-2, initial)>this is 1<Thread(Thread-3, initial)>this is 2<Thread(Thread-4, initial)>this is 3<Thread(Thread-5, initial)>this is 46
通過以上可知,我們只需要建立一個Thread對象,並運行start方法,解譯器就會建立一個子進程執行我們的target,我們建立了5個線程,但是使用threading.enumerate查看線程的數量發現有6個線程,因為當前在執行的還有一個主線程。主線程會預設等待所有的子線程結束後再結束。
import threadingfrom threading import Threadclass MyThread(Thread): def __init__(self, x): super().__init__() self.x = x def run(self): print('this is {}'.format(self.x)) time.sleep(2)def get_thread1(number=5): l_thread = (MyThread(i) for i in range(number)) for t in l_thread: print(t.name) t.start() print(len(threading.enumerate()))if __name__ == '__main__': get_thread1(5)
Thread對象有一個run方法,它就是我們需要執行的目標函數,所以我們可以通過繼承Thread對象,重寫run方法,將我們的目標代碼放置在run方法中。
Thread對象分析
class Thread: def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None): pass# Thread類是python用來建立線程的類,group:擴充保留欄位;target:目標代碼,一般是我們需要建立線程執行的目標函數。name:線程的名字,如果不指定會自動分配一個;args:目標函數的普通參數;kwargs:目標函數的索引值對參數;daemon:設定線程是否為守護線程,即是前台執行還是後台執行,預設是非守護線程,當daemon=True時,子線程為守護線程,此時主線程不會等待子線程,如果主線程完成會強制殺死所有的子線程然後退出。# 方法start():建立一個子線程並執行,該方法一個Thread執行個體只能執行一次,其會建立一個線程執行該類的run方法。run():子線程需要執行的代碼;join():主線程阻塞等待子線程直到子線程結束才繼續執行,可以設定等待逾時時間timeout.ident():線程標識符,線程未啟動之前為None,啟動後為一個int;is_alive():查看子線程是否還活著你返回一個布爾值。daemon:判斷是否是守護線程;
線程非安全與鎖
多個線程之間可以共用記憶體等資源,使得多個線程操作同一份資源的時候可能導致資源發生破壞,即線程非安全。
number = 100class MyThread(Thread): def run(self): for i in range(1000000): global number number += 1 print(number)def get_thread1(number=5): l_thread = (MyThread() for i in range(number)) for t in l_thread: t.start()if __name__ == '__main__': get_thread1(5)# 結果14394261378835224106025331503533150
上例可知,如果是同步運算的話,最終number的結果應該為5000100,但顯然不是。原因是如果線程1取得number=100時,線程切換到線程2,又取得number=100,加1賦值給number=101;如果,又切換回線程1,number加1也是101;相當於執行了兩次加1的操作,然而number=101.這就是多線程的線程非安全!
怎麼解決這個問題呢?我們看到上述代碼中number += 1是核心代碼,這個地方隨意切換線程就會造成資料破壞,因此只要我們能夠設定代碼每次執行到這裡的時候不允許切換線程就行了。這就是鎖的由來。
用鎖加入上述代碼:
number = 100mutex = threading.Lock() # 建立鎖對象class MyThread(Thread): def run(self): global number for i in range(1000000): y = mutex.acquire() # 擷取鎖 if y: # 拿到鎖就執行下面 number += 1 mutex.release() # 釋放鎖 print(number)def get_thread1(number=5): l_thread = (MyThread() for i in range(number)) for t in l_thread: t.start()if __name__ == '__main__': get_thread1(5)# 結果:44811774742053486941349737715000100
可知最後的結果符合預期,threading模組中定義了Lock類,可以很方便實現鎖機制,每次執行核心代碼之前先去擷取鎖,拿到了才能執行,拿不到預設阻塞等待。
#建立鎖mutex = threading.Lock()#鎖定mutex.acquire(blocking=True) # blocking=True,預設線程阻塞等待;如果blocking=False,線程不會等待,即上例中y會返回False,繼續執行下面的代碼,最後的結果不會符合預期#釋放mutex.release()
加鎖之後,鎖住的那段代碼變成了單線程,阻止了多線程並發執行,效率下降了;
鎖可以有多個,如果不同的線程持有不同的鎖並相互等待的話,就會造成死結;
python的多線程問題遠不止如此,還有一個曆史遺留問題-全域鎖。
死結
如果一段代碼存在兩個鎖的話,可能會出現死結現象,一旦出現死結,系統就會卡死。
number = 100mutex1 = threading.Lock() # 建立鎖對象mutex2 = threading.Lock()class MyThread1(Thread): def run(self): global number for i in range(1000): if mutex1.acquire(): # 拿到鎖就執行下面 number += 1 if mutex2.acquire(): print('this is mutex2') mutex2.release() mutex1.release() # 釋放鎖 print(number)class MyThread2(Thread): def run(self): global number for i in range(1000): if mutex2.acquire(): # 拿到鎖就執行下面 number += 1 if mutex1.acquire(): print('this is mutex2') mutex1.release() mutex2.release() # 釋放鎖 print(number)def get_thread1(): l_thread = (MyThread1(), MyThread2()) for t in l_thread: t.start()if __name__ == '__main__': get_thread1()
一般解決死結的辦法是盡量不使用多個鎖,或設計程式時避免死結,或為鎖添加逾時等待。
全域鎖(GIL)
全域鎖的前世今生不是一兩句話能講完的。可參考:Python全域解譯器鎖
總結一下就是:
- 全域鎖的存在是為了保護多線程對資料的安全訪問;
- 對於任何Python程式,不管有多少的處理器核心,任何時候都總是只有一個線程在執行;
- 全域鎖的存在使得一般情況下多線程比單線程的執行速度慢;
- python程式只有在io密集時多線程代碼效率有所提高,所以不推薦使用多線程而是多進程;更好的替代方案為協程;
number = 100number1 = 100mutex = threading.Lock()class MyThread(Thread): def run(self): global number t1 = time.time() for i in range(1000000): y = mutex.acquire() # 擷取鎖 if y: # 拿到鎖就執行下面 number += 1 mutex.release() # 釋放鎖 t2 = time.time() print(t2-t1)def get_thread1(number=5): l_thread = (MyThread() for i in range(number)) for t in l_thread: t.start()def get_thread2(n=5): global number1 for i in range(1000000*n): number1 += 1 print(number1)if __name__ == '__main__': get_thread1() t2 = time.time() get_thread2() t3 = time.time() print(t3-t2)
可知多線程的執行時間遠遠大於單線程。
結論
參考: