­

Python多任務之執行緒

  • 2019 年 10 月 10 日
  • 筆記

多任務介紹

我們先來看一下沒有多任務的程式

import time      def sing():      for i in range(5):          print("我喜歡唱")          time.sleep(1)      def dance():      for i in range(5):          print("我喜歡跳")          time.sleep(1)      def main():      sing()      dance()      pass      if __name__ == "__main__":      main()

沒有多任務的程式

運行結果:花了十秒鐘多,只能按順序執行,無法一起/同步執行

我喜歡唱  我喜歡唱  我喜歡唱  我喜歡唱  我喜歡唱  我喜歡跳  我喜歡跳  我喜歡跳  我喜歡跳  我喜歡跳

 

我們再來看一下使用了多執行緒的程式

import time  import threading      def sing():      for i in range(5):          print("我喜歡唱歌")          time.sleep(1)      def dance():      for i in range(5):          print("我喜歡跳舞")          time.sleep(1)      def main():      t1 = threading.Thread(target=sing)      t2 = threading.Thread(target=dance)      t1.start()      t2.start()      if __name__ == '__main__':      main()

使用執行緒的多任務

運行結果:花了五秒多一點,程式碼同步執行

我喜歡唱歌  我喜歡跳舞  我喜歡跳舞我喜歡唱歌    我喜歡跳舞  我喜歡唱歌  我喜歡跳舞  我喜歡唱歌  我喜歡跳舞  我喜歡唱歌

 

多任務

在這裡我們可以由多任務額外擴展一些知識,電腦是怎麼運行程式的?

單核cpu的運行原理:時間片輪轉

單核cpu同一時間只能運行一個程式,但你看到的能運行很多程式是因為單核cpu的快速切換,即把一個程式拿過來運行極短的時間比如0.00001秒,就換運行下一個程式,如此往複,就是你看到的同一時間執行多個程式。這是作業系統實現多任務的一種方式,但其實是偽多任務。

時間片輪轉的理念是,只要我切換的夠快,你看到的就是我同時做多件事情,這是作業系統的調度演算法。作業系統還有優先順序調度,比如聽歌要一直持續。

  • 如果是多核cpu同時運行多個任務,我們就稱之為並行,是真的多任務;任務數少於cpu數量;
  • 如果是單核cpu切換著運行多個任務,我們就稱之為並發,是假的多任務。任務數多於cpu數量;
  • 但因為日常中,任務數一般多於cpu核數,所以我們說的多任務一般都是並發,即假的多任務;

 

Thread多執行緒

在前面我們已經看過了執行緒實現多任務,接下來我們學習執行緒的使用方法;

通過Thread(target=xxx)創建多執行緒

執行緒的使用步驟如下:

  1. 導入threading模組;
  2. 編寫多任務所需要的的函數;
  3. 創建threading.Thread類的實例對象並傳入函數引用;
  4. 調用實例對象的start方法,創建子執行緒。

如果你還不懂怎麼使用多執行緒?沒關係,看下面這個圖就知道了

程式碼如下:

import time  import threading      def sing():      for i in range(5):          print("我喜歡唱歌")          time.sleep(1)      def dance():      for i in range(5):          print("我喜歡跳舞")          time.sleep(1)      def main():      t1 = threading.Thread(target=sing)      t2 = threading.Thread(target=dance)      t1.start()      t2.start()      if __name__ == '__main__':      main()

多執行緒的使用

注意:

  • 函數名() 表示函數的調用
  • 函數名 表示使用對函數的引用,告訴函數在哪;

 

程式碼解讀

def main():      t1 = threading.Thread(target=sing)      t2 = threading.Thread(target=dance)      t1.start()      t2.start()

每個函數在執行時都會有一個執行緒,我們稱之為主執行緒;
當我們執行到

t1 = threading.Thread(target=sing)

時,表示創建了一個Thread類的實例對象t1,並且給t1的Thread類中傳入了sing函數的引用。
同理,t2也是如此;
當我們執行到

t1.start()

時,這個實例對象就會創建一個子執行緒,去調用sing函數;然後主執行緒往下走,子執行緒去調用sing函數。
當主執行緒走到t2.start()時,再次創建一個子執行緒,子執行緒去調用dance函數,因為後面沒有程式碼了,然後主執行緒就會等待所有子執行緒的完成,再結束程式/主執行緒;可以理解為主執行緒要給子執行緒死了之後收屍,然後主執行緒再去死。

主執行緒要等子執行緒執行結束的原因:子執行緒在執行過程中會調用資源以及產生一些變數等,當子執行緒執行完之後,
主執行緒要將這些無用的資源及垃圾進行清理工作。

 

多執行緒創建執行理解

我們可以使用如下程式碼獲取當前程式中的所有執行緒;

threading.enumerate()

關於enumerate的使用,可以查看我的上一篇部落格 python內置函數之enumerate函數 ,但這裡表示的是獲取當前程式中的所有執行緒,可以不必看;

 

讓某些執行緒先運行

因為 執行緒創建完後,執行緒的執行順序是不確定的,如果我們想要讓某個執行緒先執行,可以採用time.sleep的方法。程式碼如下

import time  import threading      def sing():      for i in range(5):          print("-----sing----%d" % i)      def dance():      for i in range(5):          print("-----dance----%d" % i)      def main():      t1 = threading.Thread(target=sing)      t2 = threading.Thread(target=dance)      t1.start()      time.sleep(1)      print("sing")        t2.start()      time.sleep(1)      print("dance")        print(threading.enumerate())      if __name__ == '__main__':      main()

讓某些執行緒先運行

運行結果

我們可以看到,sing執行緒已經先運行了,但是此時查看的執行緒只有一個主執行緒,這是因為當子執行緒執行完了才執行到查看執行緒的程式碼。

 

循環查看當前運行的執行緒數

我們可以通過讓子執行緒延時執行多次,主執行緒死循環查看當前執行緒數(適當延時),即可看到當前運行的執行緒數量,當執行緒數量小於等於1時,使用break結束主執行緒。

程式碼如下

import time  import threading      def sing():      for i in range(5):          print("-----sing--%d--" % i)          time.sleep(1)      def dance():      for i in range(5):          print("-----dance--%d--" % i)          time.sleep(1)      def main():      t1 = threading.Thread(target=sing)      t2 = threading.Thread(target=dance)      t1.start()      t2.start()      while True:          t_len = len(threading.enumerate())          # print("當前運行執行緒數:%s" % t_len)          print(threading.enumerate())          if t_len <= 1:              break          time.sleep(1)      if __name__ == '__main__':      main()

循環查看當前運行的執行緒數

運行結果

可以看到,剛開始的時候只有一個是主執行緒,當子執行緒開始後,有三個執行緒,在sing子執行緒結束後,只剩兩個執行緒了,dance結束後,只有一個主執行緒。

 

驗證子執行緒的執行時間

為了驗證子執行緒的執行時間,我們可以在互動式python下運行程式碼,子執行緒調用的函數在何時執行即代表子執行緒在何時執行;

驗證結果如下

據此,我們可以判斷子執行緒的執行是在執行緒的示例對象調用start()方法之後執行的。

驗證程式碼

import threading  def sing():      print("-----sing-----")  t1 = threading.Thread(target=sing)  t1.start()  -----sing-----

 

驗證子執行緒的創建時間

驗證原理:我們可以通過計算執行緒數在各個時間段的數量來判斷子執行緒的創建時間

驗證程式碼

import time  import threading      def sing():      for i in range(5):          print("----sing----")          time.sleep(1)      def main():      print("創建實例對象之前的執行緒數:", len(threading.enumerate()))      t1 = threading.Thread(target=sing)      print("創建實例對象之後/start方法之前的執行緒數:", len(threading.enumerate()))      t1.start()      print("調用start方法之後的執行緒數:", len(threading.enumerate()))      if __name__ == '__main__':      main()

驗證子執行緒的創建時間

驗證結果

可以觀察到在調用start方法之前執行緒數一直都是1個主執行緒,由此我們可以判斷執行緒的創建時間是在調用了實例對象的start方法之後;

結合前面,我們可以得出結論,子執行緒的創建時間和執行時間是在Thread創建出來的實例對象調用了start方法之後,而子執行緒的結束時間是在調用的函數執行完成後。

 

通過繼承Thread類來創建進程

前面我們是通過子執行緒調用一個函數,那麼當函數過多時,想將那些函數封裝成一個類,我們可以不可以通過子執行緒調用一個類呢?

創建執行緒的第二種方法步驟

  1. 導入threading模組;
  2. 定義一個類,類裡面繼承threading.Thread類,裡面定義一個run方法;
  3. 然後創建這個類的實例對象;
  4. 調用實例對象的start方法,就創建了一個執行緒。

如果你創建一個執行緒的時候是通過 類繼承一個Thread類來創建的,必須在裡面定義run方法,當你調用start方法的時候,會自動調用run方法,接下來執行緒執行的就是run方法裡面的程式碼。

通過繼承Thread類來創建進程示例程式碼

import time  import threading      class TestThread(threading.Thread):      def run(self):          print("---run---")          for i in range(3):              msg = "我是%s,i--->%s" % (self.name, str(i))  # self.name中保存的是當前執行緒的名字              print(msg)              time.sleep(1)      def main():      t1 = TestThread()      t1.start()      if __name__ == '__main__':      main()

通過繼承Thread類來創建進程

運行結果

---run---  我是Thread-1,i--->0  我是Thread-1,i--->1  我是Thread-1,i--->2

知識點

  • 這種方法適用於一個執行緒裡面要做的事情比較複雜,要封裝成幾個函數來做,那麼我們就將它封裝成一個類。
  • 在類中定義其他的幾個函數,可以在run裡面進行調用這幾個函數。
  • 創建執行緒時使用哪種方法比較好?哪個簡單使用哪個。

注意:

一個實例對象只能創建一個執行緒;
通過繼承Thread類來創建進程時,不會自動調用類中除run函數的其他函數,如果想要調用其他可數,可以在run方法中使用self.xxx()來調用。

多執行緒共享變數

在函數中修改全局變數,如果是數字等不可變類型,要用global聲明之後才能修改,如果是列表等可變類型,就可以不用聲明,直接append等對列表內容進行修改,但,如果不是對列表內容進行修改,而是指向一個新的列表,就需要使用global聲明;

在全局變數中,如果是對引用的數據進行修改,那麼不需要使用global,如果是對全局變數的引用進行修改(直接換一個引用地址),那麼就需要使用global,同時,我們也應注意全局變數是可變類型還是不可變類型,比如數字,不可變,就只能通過修改變數的引用來進行修改全局變數了,所以需要global;

 

驗證多執行緒中共享全局變數

驗證原理:

定義一個全局變數,在函數1中加1,在函數2中查看,讓執行緒控制的函數1先執行,如果執行緒函數2的查看結果和函數1的查看結果一樣,那麼就證明多執行緒之間共享全局變數。

程式碼驗證

import time  import threading      g_num = 100      def sing():      global g_num      g_num += 1      print("---sing中的g_num: %d---" % g_num)      time.sleep(1)      def dance():      print("---dance中的g_num: %d---" % g_num)      time.sleep(1)      def main():      t1 = threading.Thread(target=sing)      t2 = threading.Thread(target=dance)      t1.start()      t2.start()      print("---主執行緒中的g_num: %d---" % g_num)      if __name__ == '__main__':      main()

多執行緒之間共享全局變數驗證

運行結果

---sing中的g_num: 101---  ---dance中的g_num: 101---  ---主執行緒中的g_num: 101---

如上程式碼我們可知,多執行緒之間共享全局變數。

 

我們可以將多執行緒之間共享去全局變數理解為:
一個房子裡面有幾個人,一個人就是一個執行緒,每個人有自己私有的東西資源,但在這個大房子裡面,也有些共有的東西,比如說唯一一台飲水機的水,有一個人喝了一半,拿下一個人來接水,也只剩下一半了,這個飲水機裡面的誰就是全局變數。

 

多執行緒給子執行緒傳參

給子執行緒傳參數語法如下

g_nums = [11, 22]    t1 = threading.Thread(target=sing, args=(g_num,))

給子執行緒傳參示例程式碼

import time  import threading      def sing(temp):      temp.append(33)      print("---sing中的g_nums: %s---" % str(temp))      time.sleep(1)      def dance(temp):      print("---dance中的g_nums: %s---" % str(temp))      time.sleep(1)      g_nums = [11, 22]      def main():      t1 = threading.Thread(target=sing, args=(g_nums,))      t2 = threading.Thread(target=dance, args=(g_nums,))      t1.start()      time.sleep(1)      t2.start()      time.sleep(1)      print("---主執行緒中的g_nums: %s---" % str(g_nums))      if __name__ == '__main__':      main()

給子執行緒傳參數

運行結果

---sing中的g_nums: [11, 22, 33]---  ---dance中的g_nums: [11, 22, 33]---  ---主執行緒中的g_nums: [11, 22, 33]---

 

多執行緒之間共享問題:資源競爭

共享全局變數存在資源競爭的問題,兩個執行緒同時使用或者修改就會存在問題,一個修改一個使用不會存在;
傳參100的時候可能不會出現問題,因為數字較小,概率也小點;但傳參1000000的時候,數字變大,概率也變大;

num += 1可以分解為三句,獲取num的值,給值加1,給num重賦值;有可能當執行緒1執行12句,正打算執行3句的時候,cpu就將資源給了執行緒2,而執行緒2同理,然後又執行執行緒1的第3句,因此執行緒1 +1,存儲全局變數為1;輪到執行緒2 +1,存儲全局變數也為1;問題就出現了,本來加兩次應該是2的,但全局變數還是1。

資源競爭程式碼示例

import time  import threading      g_num = 0      def add1(count):      global g_num      for i in range(count):          g_num += 1      print("the g_num of add1:", g_num)      def add2(count):      global g_num      for i in range(count):          g_num += 1      print("the g_num of add2:", g_num)      def main():      t1 = threading.Thread(target=add1, args=(1000000,))      t2 = threading.Thread(target=add2, args=(1000000,))        t1.start()      t2.start()      time.sleep(3)      print("the g_num of main:", g_num)      if __name__ == '__main__':      main()

共享變數的資源競爭問題

運行結果

the g_num of add1: 1096322  the g_num of add2: 1294601  the g_num of main: 1294601

 

互斥鎖解決資源競爭問題

原子性操作:要麼不做,要麼做完;

互斥鎖:一個人做某事的時候,別人不允許做這件事,必須需得等到前面的人做完了這件事,才能接著做,例子景點上廁所。

互斥鎖語法

# 創建鎖:  mutex = threading.Lock()  # 上鎖:  mutex.acquire()  # 解鎖:  mutex.release()

使用互斥鎖解決資源競爭問題

import time  import threading      g_num = 0      def add1(num):      global g_num      for i in range(num):          mutex.acquire()          g_num += 1          mutex.release()      print("the g_num of add1:", g_num)      def add2(num):      global g_num      for i in range(num):          mutex.acquire()          g_num += 1          mutex.release()      print("the g_num of add2:", g_num)      mutex = threading.Lock()      def main():      t1 = threading.Thread(target=add1, args=(1000000,))      t2 = threading.Thread(target=add2, args=(1000000,))      t1.start()      t2.start()        time.sleep(2)      print("the g_num of main:", g_num)      if __name__ == '__main__':      main()

使用互斥鎖解決資源競爭的問題

運行結果

the g_num of add2: 1901141  the g_num of add1: 2000000  the g_num of main: 2000000

可以看出,使用互斥鎖可以解決資源競爭的問題。

 

死鎖問題

使用互斥鎖特別是多個互斥鎖的時候,特別容易產生死鎖,就是你在等我的資源,我在等你的資源;

 

本章內容總結

執行緒的生命周期

  • 從程式開始執行到結束,一直都有一條主執行緒
  • 如果主執行緒先死了,那麼正在運行的子執行緒也會死。
  • 子執行緒開始創建是在調用t.start()時,而不是創建Thread的實例化對象時。
  • 子執行緒的開始執行是在調用t.start()時;
  • 子執行緒的死亡時間是在子執行緒調用的函數執行完成後;
  • 執行緒創建完後,執行緒的執行順序是不確定的;
  • 如果想要讓某個執行緒先執行,可以採用time.sleep的方法。

 

創建多執行緒的兩種方式

通過Thread(target=xxx)創建多執行緒

  1. 導入threading模組;
  2. 編寫多任務所需要的的函數;
  3. 創建threading.Thread類的實例對象並傳入函數引用;
  4. 調用實例對象的start方法,創建子執行緒。

通過繼承Thread類來創建進程

  1. 導入threading模組;
  2. 定義一個類,類裡面繼承threading.Thread類,裡面定義一個run方法;
  3. 然後創建這個類的實例對象;
  4. 調用實例對象的start方法,就創建了一個執行緒。

 

多執行緒理解

  • 創建多執行緒可以理解為創建執行緒做準備;
  • start() 則是準備好後直接創建並運行執行緒;
  • 主執行緒要等子執行緒結束後在結束是為了清理子執行緒中可能產生的垃圾;

 

多執行緒共享全局變數

  • 子執行緒和子執行緒之間共享全局變數;
  • 給子執行緒傳參可以使用  threading.Thread(target=sing, args=(g_num,)) 進行傳參;
  • 多執行緒之間可能存在資源競爭的問題;
  • 可以使用互斥鎖解決資源競爭的問題;