標籤:elf 多進程 實現 積累 enumerate 任務 弊端 sel run
1.什麼是線程?
進程是作業系統分配程式執行資源的單位,而線程是進程的一個實體,是CPU調度和分配的單位。一個進程肯定有一個主線程,我們可以在一個進程裡建立多個線程來實現多任務。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2.一個程式實現多任務的方法
如所示,實現多任務,我們可以用幾種方法。
(1)在主進程裡面開啟多個子進程,主進程和多個子進程一起處理任務。
有關多個進程實現多任務,可以參考我的博文:https://www.cnblogs.com/chichung/p/9532962.html
(2)在主進程裡開啟多個子線程,主線程和多個子線程一起處理任務。
(3)在主進程裡開啟多個協程,多個協程一起處理任務。
有關多個協程實現多任務,可以參考我的博文:https://www.cnblogs.com/chichung/p/9544566.html
注意:因為用多個線程一起處理任務,會產生安全執行緒問題,所以在開發中一般使用多進程+多協程來實現多任務。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
3.多線程的建立方式
import threadingp1 = threading.Thread(target=[函數名],args=([要傳入函數的參數]))p1.start() # 啟動p1線程
我們來類比一下多線程實現多任務。
假如你在用網易雲音樂一邊聽歌一邊下載。網易雲音樂就是一個進程。假設網易雲音樂內部程式是用多線程來實現多任務的,網易雲音樂開兩個子線程。一個用來緩衝音樂,用於現在的播放。一個用來下載使用者要下載的音樂的。這時候的代碼架構是這樣的:
import threadingimport timedef 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 threadingimport timeclass 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 0Thread-3 0Thread-2 0Thread-1 1Thread-2 1Thread-3 1Thread-1 2Thread-3 2Thread-2 2Thread-1 3Thread-2 3Thread-3 3Thread-1 4Thread-2 4Thread-3 4
運行時無序的,說明已經啟用了多任務。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
4.線程何時開啟,何時結束
(1)子線程何時開啟,何時運行 當調用thread.start()時 開啟線程,再運行線程的代碼(2)子線程何時結束 子線程把target指向的函數中的語句執行完畢後,或者線程中的run函數代碼執行完畢後,立即結束當前子線程(3)查看當前線程數量 通過threading.enumerate()可枚舉當前啟動並執行所有線程(4)主線程何時結束 所有子線程執行完畢後,主線程才結束
import threadingimport timedef run(): for i in range(5): time.sleep(1) print(i)t1 = threading.Thread(target=run)t1.start()print("我會在哪裡出現")輸出:我會在哪裡出現01234
為什麼主進程(主線程)的代碼會先出現呢?因為CPU採用時間片輪詢的方式,如果輪詢到子線程,發現他要休眠1s,他會先去運行主線程。所以說CPU的時間片輪詢方式可以保證CPU的最佳運行。
那如果我想主進程輸出的那句話運行在結尾呢?該怎麼辦呢?這時候就需要用到 join() 方法了。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
5.線程的 join() 方法
import threadingimport timedef run(): for i in range(5): time.sleep(1) print(i)t1 = threading.Thread(target=run)t1.start()t1.join() print("我會在哪裡出現")輸出:01234我會在哪裡出現
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 += 110 temp = num11 num = temp + 112 print(num)13 14 15 def work2(loop):16 global num17 for i in range(loop):18 # 等價於 num += 119 temp = num20 num = temp + 121 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 threadingimport timenum = 0lock = 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多線程實現多任務