python多線程實現多任務
1.什麽是線程?
進程是操作系統分配程序執行資源的單位,而線程是進程的一個實體,是CPU調度和分配的單位。一個進程肯定有一個主線程,我們可以在一個進程裏創建多個線程來實現多任務。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2.一個程序實現多任務的方法
如上圖所示,實現多任務,我們可以用幾種方法。
(1)在主進程裏面開啟多個子進程,主進程和多個子進程一起處理任務。
有關多個進程實現多任務,可以參考我的博文:https://www.cnblogs.com/chichung/p/9532962.html
(2)在主進程裏開啟多個子線程,主線程和多個子線程一起處理任務。
(3)在主進程裏開啟多個協程,多個協程一起處理任務。
有關多個協程實現多任務,可以參考我的博文:https://www.cnblogs.com/chichung/p/9544566.html
註意:因為用多個線程一起處理任務,會產生線程安全問題,所以在開發中一般使用多進程+多協程來實現多任務。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
3.多線程的創建方式
import threading p1 = threading.Thread(target=[函數名],args=([要傳入函數的參數])) p1.start()# 啟動p1線程
我們來模擬一下多線程實現多任務。
假如你在用網易雲音樂一邊聽歌一邊下載。網易雲音樂就是一個進程。假設網易雲音樂內部程序是用多線程來實現多任務的,網易雲音樂開兩個子線程。一個用來緩存音樂,用於現在的播放。一個用來下載用戶要下載的音樂的。這時候的代碼框架是這樣的:
import threading import time def listen_music(name): while True: time.sleep(1) print(name,"正在播放音樂") def download_music(name): while True: time.sleep(2) print(name,"正在下載音樂") if __name__ == ‘__main__‘: p1 = threading.Thread(target=listen_music,args=("網易雲音樂",)) p2 = threading.Thread(target=download_music,args=("網易雲音樂",)) p1.start() p2.start() 輸出: 網易雲音樂 正在播放音樂 網易雲音樂 正在下載音樂 網易雲音樂 正在播放音樂 網易雲音樂 正在播放音樂 網易雲音樂 正在下載音樂 網易雲音樂 正在播放音樂 網易雲音樂 正在播放音樂 網易雲音樂 正在下載音樂 網易雲音樂 正在播放音樂 網易雲音樂 正在播放音樂 網易雲音樂 正在播放音樂 ...... ......
觀察上面的輸出代碼可以知道:
1.CPU是按照時間片輪詢的方式來執行子線程的。cpu內部會合理分配時間片。時間片到a程序的時候,a程序如果在休眠,就會自動切換到b程序。
2.嚴謹來說,CPU在某個時間點,只在執行一個任務,但是由於CPU運行速度和切換速度快,因為看起來像多個任務在一起執行而已。
除了上面的方法創建線程,還有另一種方法。可以編寫一個類,繼承threaing.Thread類,然後重寫父類的run方法。
import threading import time class MyThread(threading.Thread): def run(self): for i in range(5): time.sleep(1) print(self.name,i) t1 = MyThread() t2 = MyThread() t3 = MyThread() t1.start() t2.start() t3.start() 輸出: Thread-1 0 Thread-3 0 Thread-2 0 Thread-1 1 Thread-2 1 Thread-3 1 Thread-1 2 Thread-3 2 Thread-2 2 Thread-1 3 Thread-2 3 Thread-3 3 Thread-1 4 Thread-2 4 Thread-3 4
運行時無序的,說明已經啟用了多任務。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
4.線程何時開啟,何時結束
(1)子線程何時開啟,何時運行
當調用thread.start()時 開啟線程,再運行線程的代碼
(2)子線程何時結束
子線程把target指向的函數中的語句執行完畢後,或者線程中的run函數代碼執行完畢後,立即結束當前子線程
(3)查看當前線程數量
通過threading.enumerate()可枚舉當前運行的所有線程
(4)主線程何時結束
所有子線程執行完畢後,主線程才結束
import threading import time def run(): for i in range(5): time.sleep(1) print(i) t1 = threading.Thread(target=run) t1.start() print("我會在哪裏出現") 輸出: 我會在哪裏出現 0 1 2 3 4
為什麽主進程(主線程)的代碼會先出現呢?因為CPU采用時間片輪詢的方式,如果輪詢到子線程,發現他要休眠1s,他會先去運行主線程。所以說CPU的時間片輪詢方式可以保證CPU的最佳運行。
那如果我想主進程輸出的那句話運行在結尾呢?該怎麽辦呢?這時候就需要用到 join() 方法了。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
5.線程的 join() 方法
import threading import time def run(): for i in range(5): time.sleep(1) print(i) t1 = threading.Thread(target=run) t1.start() t1.join() print("我會在哪裏出現") 輸出: 0 1 2 3 4 我會在哪裏出現
join() 方法可以阻塞主進程(註意只能阻塞主進程,其他子進程是不能阻塞的),直到 t1 子線程執行完,再解阻塞。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
6.線程可以共享全局變量
這個稍微實驗下就可以知道了,所以這裏不廢話。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
7.多線程共享全局變量出現的問題
我們開兩個子線程,全局變量是0,我們每個線程對他自加1,每個線程加一百萬次,這時候就會出現問題了,來,看代碼:
1 import threading 2 import time 3 4 num = 0 5 6 def work1(loop): 7 global num 8 for i in range(loop): 9 # 等價於 num += 1 10 temp = num 11 num = temp + 1 12 print(num) 13 14 15 def work2(loop): 16 global num 17 for i in range(loop): 18 # 等價於 num += 1 19 temp = num 20 num = temp + 1 21 print(num) 22 23 24 if __name__ == ‘__main__‘: 25 t1 = threading.Thread(target=work1,args=(1000000,)) 26 t2 = threading.Thread(target=work2, args=(1000000,)) 27 t1.start() 28 t2.start() 29 30 while len(threading.enumerate()) != 1: 31 time.sleep(1) 32 print(num) 33 34 輸出: 35 1459526 # 第一個子線程結束後全局變量一共加到這個數 36 1588806 # 第二個子線程結束後全局變量一共加到這個數 37 1588806 # 兩個線程都結束後,全局變量一共加到這個數
奇怪了,我不是每個線程都自加一百萬次嗎?照理來說,應該最後的結果是200萬才對的呀。問題出在哪裏呢?
我們知道CPU是采用時間片輪詢的方式進行幾個線程的執行。
假設我CPU先輪詢到work1(),num此時為100,在我運行到第10行時,時間結束了!此時,賦值了,但是還沒有自加!即temp=100,num=100。
然後,時間片輪詢到了work2(),進行賦值自加。num=101了。
又回到work1()的斷點處,num=temp+1,temp=100,所以num=101。
就這樣!num少了一次自加!
在次數多了之後,這樣的錯誤積累在一起,結果只得到158806!
這就是線程安全問題!
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
8.GIL鎖(互斥鎖)可以彌補部分線程安全問題。註意是部分!至於GIL鎖的弊端請關照我的這一篇博文
當多個線程幾乎同時修改某一個共享數據的時候,需要進行同步控制
線程同步能夠保證多個線程安全訪問競爭資源,最簡單的同步機制是引入互斥鎖。
互斥鎖為資源引入一個狀態:鎖定/非鎖定
某個線程要更改共享數據時,先將其鎖定,此時資源的狀態為“鎖定”,其他線程不能更改;直到該線程釋放資源,將資源的狀態變成“非鎖定”,其他的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個線程進行寫入操作,從而保證了多線程情況下數據的正確性。
GIL鎖有三個常用步驟
lock = threading.Lock() # 取得鎖 lock.acquire() # 上鎖 lock.release() # 解鎖
下面讓我們用GIL鎖來解決上面例子的線程安全問題。
import threading import time num = 0 lock = threading.Lock() # 取得鎖 def work1(loop): global num for i in range(loop): # 等價於 num += 1 lock.acquire() # 上鎖 temp = num num = temp + 1 lock.release() # 解鎖 print(num) def work2(loop): global num for i in range(loop): # 等價於 num += 1 lock.acquire() # 上鎖 temp = num num = temp + 1 lock.release() # 解鎖 print(num) if __name__ == ‘__main__‘: t1 = threading.Thread(target=work1,args=(1000000,)) t2 = threading.Thread(target=work2, args=(1000000,)) t1.start() t2.start() while len(threading.enumerate()) != 1: time.sleep(1) print(num) 輸出: 1945267 # 第一個子線程結束後全局變量一共加到這個數 2000000 # 第二個子線程結束後全局變量一共加到這個數 2000000 # 兩個線程都結束後,全局變量一共加到這個數
python多線程實現多任務