Python 實例:多線程

2021-03-02 計算機與網絡安全

一次性付費進群,長期免費索取教程,沒有付費教程。

教程列表見微信公眾號底部菜單

進微信群回復公眾號:微信群;QQ群:460500587

微信公眾號:計算機與網絡安全

ID:Computer-network

不管哪種程式語言,多線程都是必不可少的。這種提高工作效率的神器,怎麼重視都不過分。多線程,就是將多個線性順序執行的過程變成並行運行。並行的數量越多,效率就越高。如果需要放空一個水池的水,打開多個放水孔的效率顯然要比打開一個放水孔的效率高。這種做法是典型的以資源換時間。

首先設計一個簡單的程序threadingOrderRun.py,打開Putty連接到Linux,執行命令:threadingOrderRun.py的代碼如下:

1    #!/usr/bin/env python3

2    #-*- coding:utf-8 -*-

3    __author__ = 'hstking hst_king@hotmail.com'

4

5    import time

6

7    def showName(name):

8     nowTime = time.strftime('%H:%M:%S', time.localtime(time.time()))

9     print('My name is function-%s, now time: %s ' %(name, nowTime))

10     time.sleep(1)

11

12

13    if __name__ == '__main__':

14     for i in range(20): #沒有開多線程的情況下,執行20次操作

15         showName(i)

這個函數的功能很簡單,就是從列表中提取名字作為showName的參數,然後顯示名字和當前時間。最後的time.sleep(1)是因為這個過程太快了,所以休眠1秒以便於觀察。這是一種理所當然的順序操作方法,但理所當然的操作並不是最有效率的操作。這種方法完全就是線性操作,從列表中取出一個元素,就用函數處理一個元素。現在運行程序試試,執行命令:

time python3 threadingOrderRun.py

圖1  線性順序執行

從圖1中可以看出,該程序執行時間為20.070秒。如果列表比較小,那還可以忍受。如果這個列表比較大呢?1000個元素至少得1000秒,那就完全無法接受了。明明計算機有多餘的資源,卻花費了這麼多的時間。完全可以利用資源來換取時間,用多線程操作。在同一時間內運行多個線程(事實上線程並不是同時運行的,是基於時間片輪轉的方式執行。但對於操作者來說,跟同時運行並沒有什麼區別)。這樣雖然佔用了一定的資源,但大大地節省了時間。畢竟絕大多數的情況下都是希望時間優先的。在Python中,多線程的模塊是threading模塊。如果要完全深入了解threading模塊,恐怕得好好地讀一下Python的說明文檔。如果只是要使用,那就很簡單了。這裡需要注意的是,使用threading模塊進行多線程操作有兩種方法。一種是以函數的形式調用,一種是以類的方式調用。

先以函數的形式使用多線程。打開Putty連接到Linux,執行命令:

vi threadingOfFunction.py
threadingOfFunction.py的代碼如下:

1    #!/usr/bin/env python3

2    #-*- coding:utf-8 -*-

3    __author__ = 'hstking hst_king@hotmail.com'

4

5    import time

6    import threading

7

8     def showName(threadNum ,name):

9     nowTime = time.strftime('%H:%M:%S', time.localtime(time.time()))

10     print('I am thread-%d ,My name is function-%s, now time: %s '%(threadNum, name, nowTime))

11     time.sleep(1)

12

13     if __name__ == '__main__':

14     print('I am main ...')

15     names = range(20)

16     threadNum = 1 #threadNum 指的是線程執行的批次。

17     threadPool = [] #線程池

18     while names:

19         for i in range(6):

20             try: #這裡需要考慮列表已經讀取完畢的情況

21                 name = names.pop()

22             except IndexError as e:

23                 print('The list is empty')

24                 break

25             else:

26                 t = threading.Thread(target=showName, args=(i, name, ))

27             threadPool.append(t)

28             t.start()

29         while threadPool: #也可以用for循環,然後清空threadPool線程池

30             t = threadPool.pop()

31             t.join() #使用join是為了阻塞主函數,意思是必須將t這個函數執行完畢後才能繼續執行主函數

32         threadNum += 1

33         print('--\r\n')

34     print('main is over ...')

以函數的形式使用多線程,就是用threading.Thread(target=functionName, args=(arguments,))的方法代入需要進行多線程操作的函數和函數所需的參數(如果沒有參數更好)。

如果代入的函數有一個參數,那麼args要寫成args=(arg1, )。如果有兩個參數,那麼args就要寫成args=(arg1, arg2, )。總之在參數元組的最後要留出一個空位。

在程序的第29~31行,使用了join函數是為了阻塞主函數,意思是主線程必須等待多線程執行完畢後才能正常結束。其實這裡也可以不用join函數,多線程的daemon屬性默認是false。這種情況下主線程本來就是要等待多線程執行完畢才會結束的。Join函數更多的是用在多進程。

time python3 threadingOfFunction.py

圖2  函數式多線程

從圖2可以看出,這次操作只花費了4.002秒。與順序操作的20.045秒相比節約了一大半的時間。在計算機可以承受的範圍內,調大線程的數量,還可以將運行時間進一步縮短。

threadingOfClass.py的代碼如下:

1    #!/usr/bin/env python3

2    #-*- coding:utf-8 -*-

3    __author__ = 'hstking hst_king@hotmail.com'

4

5    import time

6    import threading

7

8    class ShowName(threading.Thread): #這裡的類名要大寫,該類繼承於threading.Thread類

9     def __init__(self, threadNum ,name):

10         threading.Thread.__init__(self) #這一步是必不可少的

11         self.name = name

12         self.threadNum = threadNum

13

14     def run(self):

15         nowTime = time.strftime('%H:%M:%S', time.localtime(time.time()))

16         print('I am thread-%d ,My name is function-%s, now time: %s '%(self.threadNum, self.name, nowTime))

17         time.sleep(1)

18

19     if __name__ == '__main__':

20     print('I am main ...')

21     names = [x for x in range(20)]

22     threadNum = 1

23     threadPool = []

24     while names:

25         for i in range(6):

6             try: #考慮線程已經讀取完畢的情況

27                 name = names.pop()

28             except IndexError as e:

29                 print('The List is empty')

30                 break

31             else:

32                 t = ShowName(i, name) #這裡調用ShowName幾乎與直接調用threading.Thread是一樣的。

33             threadPool.append(t)

34             t.start()

35         while threadPool:

36             t = threadPool.pop()

37          t.join()

38      threadNum += 1

39      print('===============\r\n')

40      print('main is over ...')

time python3 threadingOfClass.py

圖3  類式多線程

從圖3中可以看到,threadingOfClass.py的運行時間是4.019秒,跟threadingOfFunction.py的4.002相當接近,說明這兩種方法從效率上來說沒有什麼區別,任選一種使用都可以。

這兩種方法都是對類的調用。所謂函數式的調用是對threading.Thread類的直接調用,而類式的調用則是先繼承threading.Thread類,重載子類的函數後再調用子類的方式調用,只是前者看起來更像函數調用而已。

多線程的好處在於可以並行運行重複性的工作,大大地減少了運行時間,但使用多線程也難免會忙中出錯。例如,重複地往一個文件內寫入單行內容。如果單線程的線性執行,很難出錯。如果多線程執行時那就不一定了。當多個線程同時向文件內寫入內容時,會不會造成一個線程寫入成功、其他的線程都做了無用功呢?不妨測試一下。

vi threadingWithoutLock.py
threadingWithoutLock.py的代碼如下:

1    #!/usr/bin/env python3

2    #-*- coding:utf-8 -*-

3    __author__ = 'hstking hst_king@hotmail.com'

4

5    import time

6    import threading

7    import codecs

8

9    def showName(threadNum ,name):

10     with codecs.open('test.txt', 'a', 'utf-8') as fp:

11         nowTime = time.strftime('%H:%M:%S',time.localtime(time.time()))

12         fp.write('I am thread-%d ,My name is function-%s, now time:%s\r\n ' %(threadNum, name, nowTime))

13         print('I am thread-%d ,My name is function-%s, now time: %s '%(threadNum, name, nowTime))

14     time.sleep(1)

15

16

17     if __name__ == '__main__':

18     with codecs.open('test.txt', 'w', 'utf-8') as fp:

19         fp.write('')

20     print('I am main ...')

21     names = [x for x in range(100)]

22     threadNum = 1

23     threadPool = []

24     while names:

25         for i in range(13):

26             try:

27                 name = names.pop()

28             except IndexError as e:

29                 print('The list is empty')

30                 break

31             else:

32                 t = threading.Thread(target=showName, args=(i, name, ))

33             threadPool.append(t)

34             t.start()

35         while threadPool:

36             t = threadPool.pop()

37             t.join()

38         threadNum += 1

39     print('main is over ...')

這個程序與之前的多線程程序基本上沒什麼區別,只是增加了總共線程的數量(100個),並將輸出寫入了一個文件中。當執行的總線程比較少,同時執行的線程也不多的情況下也許不會出現問題。一旦數量上去了,問題就比較突出了。理論上names列表有100個元素,那麼就應該有100行字符串寫入到了test.txt文本中。

time python3 threadingWithoutLock.py

圖4  Linux下未使用線程鎖的多線程

可以看出在Linux下還是正常的結果。現在將這個程序複製到Windows下執行,執行結果如圖5所示。

圖5  Windows下未使用線程鎖的多線程

只有91行的數據寫入到了文件內,還有9行數據丟失了。為了避免類似的情況,Python採用了線程鎖的方法確保文件的安全。使用線程鎖鎖定資源,避免幹擾。

在Python的多線程中有兩種鎖,一種是互斥鎖,另一種是可重入鎖。這兩者的區別是互斥鎖只能鎖定一次,解鎖一次,而可重入鎖可以鎖定多次。一般都是使用的互斥鎖。至於互斥鎖的效果如何,可以將上個例子稍微修改一下,加上互斥鎖測試一下即可。

打開Putty連接到Linux,執行命令:

threadingWithLock.py的代碼如下:

1    #!/usr/bin/env python3

2    #-*- coding:utf-8 -*-

3    __author__ = 'hstking hst_king@hotmail.com'

4

5    import time

6    import threading

7    import codecs

8

9     def showName(threadNum ,name):

10     mutex.acquire()

11     with codecs.open('test.txt', 'a', 'utf-8') as fp: #寫入到test.txt文件內

12         nowTime = time.strftime('%H:%M:%S', time.localtime(time.time()))

13         fp.write('I am thread-%d ,My name is function-%s, now time:%s\r\n ' %(threadNum, name, nowTime))

14         print('I am thread-%d ,My name is function-%s, now time: %s '

%(threadNum, name, nowTime))

15     mutex.release()

16     time.sleep(1)

17

18

19     if __name__ == '__main__':

20     with codecs.open('test.txt', 'w', 'utf-8') as fp:

21         fp.write('')

22     print('I am main ...')

23     mutex = threading.Lock()

24     names = [x for x in range(100)]

25     threadNum = 1

26     threadPool = []

27     while names:

28         for i in range(13):

29             try:

30              name = names.pop()

31          except IndexError as e:

32              print('The list is empty')

33              break

34          else:

35              t = threading.Thread(target=showName, args=(i, name, ))

36          threadPool.append(t)

37          t.start()

38      while threadPool:

39          t = threadPool.pop()

40          t.join()

41      threadNum += 1

42      print('main is over ...')

對比一下代碼,很容易理解,就是所有線程在寫入文件前加上一個互斥鎖,鎖定資源。等寫入完畢後再釋放互斥鎖。這樣就確保數據一定可以寫入到文件內了。

執行程序測試一下。運行命令:

time python3 threadingWithLock.py

圖6  Linux下使用線程鎖的多線程

檢查一下test.txt文件,正好100行,符合設計的結果。再將threadingWithLock.py程序複製到Windows下運行一下。執行結果如圖7所示。

圖7  Windows下使用線程鎖的多線程

通過對比可以看出使用線程鎖的情況下數據才能保證安全。程序才能按照設計的方式運行。如果不使用線程鎖,Linux下沒什麼問題(這只是特例,並不代表不使用線程鎖Linux下就是安全的)。Windows下必定會出現這樣那樣的問題。

微信公眾號:計算機與網絡安全

ID:Computer-network

【推薦書籍】

相關焦點

  • Python多線程實戰
    點擊上方「Python人工智慧編程」,馬上關注目的:(1)了解python線程執行原理(2)掌握多線程編程與線程同步(3)了解線程池的使用1 線程基本概念1.1 線程是什麼?線程是指進程內的一個執行單元,也是進程內的可調度實體.
  • Python入門基礎之socket多線程編程,TCP伺服器和客戶端通信
    在上一篇文章中,我介紹了一下python3 socket編程的基礎,包括TCP、UDP客戶端和伺服器的建立過程(連結在最下方)。不過那個只是單線程的,伺服器一次只能和一個客戶端會話,多個客戶端的話只能等待。我們平時的應用中,伺服器肯定是要並發的,所以,今天將介紹socket編程的多線程編程。
  • 作為數據科學家你應該知道這些 python 多線程、進程知識
    並行處理可以用 python 以兩種不同的方式實現:多處理和線程。多處理與線程:理論基本上,多處理和線程是實現並行計算的兩種方法,分別使用進程和線程作為處理代理。為了理解它們的工作原理,我們必須搞清楚什麼是進程和線程。進程進程是正在執行的電腦程式的實例。
  • 理解Python中多線程
    p=2604(點擊尾部閱讀原文前往)雖然Python中由於GIL的機制致使多線程不能利用機器多核的特性,但是多線程對於我們理解並發模型以及底層操作非常有用。線程的有兩種使用方法,一種是在函數使用,一種是放在類中使用。
  • python核心編程之多線程教程-threading
    多線程-threadingpython的thread模塊是比較底層的模塊,python的threading模塊是對thread做了一些包裝的,可以更加方便的被使用1.time.sleep(1)if __name__ == "__main__":for i in range(5):saySorry()運行結果:多線程執行#coding=utf-8import threadingimport timedef saySorry()
  • python教程之十二多線程
    進程與線程機器分配資源(包括內存、CPU等)的最小單位是進程,任務調度採用的是時間片輪轉的搶佔式調度方式。一個進程可以有一個或多個線程,各個線程之間共享程序的內存空間(也就是所在進程的內存空間)。一個標準的線程由線程ID,當前指令指針PC,寄存器和堆棧組成,進程由內存空間和一個或多個線程組成,以下是進程與線程的區別。1.
  • Python 多線程入門,這一篇文章就夠了
    線程與進程 既然提到多線程,多進程了,那就有必要先了解下線程和進程的相關概念了。要不然的話後面的內容理解起來也是有點費勁的。提到進程啊,我想你肯定是不陌生的,我們在電腦上打開一個軟體,就是開啟了一個進程,更具體的來說,Windows 系統你可以通過資源管理器進行查看當前電腦啟動的進程數。
  • 作為數據科學家你應該知道這些 python 多線程、進程知識
    python 為並行化提供了兩個內置庫:多處理和線程。在這篇文章中,我們將探討數據科學家如何在兩者之間進行選擇,以及在這樣做時應注意哪些因素。眾所周知,數據科學是處理大量數據並從中提取有用見解的科學。通常情況下,我們對數據執行的操作很容易並行化,這意味著不同的處理代理可以一次對數據執行一個操作,最後進行組合以獲得完整的結果。
  • 跟我學Java編程—用實例說明多線程的使用場景及方法
    任務要求簡單模擬1000個用戶的並發訪問,檢索功能分別採用單線程和多線程實現,比較在1000個用戶的並發訪問下,單線程和多線程的檢索效率。1、線程的創建和啟動Java提供了兩種創建線程的方式。Runnable接口中只有一個run()方法,用來定義線程運行體。代碼如下:定義好MyRunner類後,需要把MyRunner類的實例作為參數傳入到Thread的構造方法中,來創建一個新線程。
  • Python sleep()函數用法:線程睡眠
    位於 time 模塊中的 sleep(secs) 函數,可以實現令當前執行的線程暫停 secs 秒後再繼續執行。
  • 看幾段爬蟲代碼,詳解Python多線程、多進程、協程
    URL時必然會引起等待示例代碼就是典型的串行邏輯,parse_1將url和循環數傳遞給parse_2,parse_2請求並返回狀態碼後parse_1繼續迭代一次,重複之前步驟因為CPU在執行程序時每個時間刻度上只會存在一個線程,因此多線程實際上提高了進程的使用率從而提高了CPU的使用率實現多線程的庫有很多,這裡用concurrent.futures
  • Python並發編程之線程中的信息隔離
    比如說,咱有兩個線程,線程A裡的變量,和線程B裡的變量值不能共享。這就是 信息隔離 。你可能要說,那變量名取不一樣不就好啦?是的,如果所有的線程都不是由一個class實例化出來的同一個對象,確實是可以。這個問題我們暫且掛著,後面我再說明。那麼,如何實現 信息隔離 呢?
  • Python爬蟲從入門到精通(3): BeautifulSoup用法總結及多線程爬蟲爬取糗事百科
    我們還會利用requests庫和BeauitfulSoup來爬取糗事百科上的段子, 並對比下單線程爬蟲和多線程爬蟲的爬取效率。什麼是BeautifulSoup及如何安裝BeautifulSoup是一個解析HTML或XML文件的第三方庫。
  • 很全的 Python 面試題
    因為實例方法的調用離不開實例,我們需要把實例自己傳給函數,調用的時候是這樣的a.foo(x)(其實是foo(a, x)).類方法一樣,只不過它傳遞的是類而不是實例,A.class_foo(x).注意這裡的self和cls可以替換別的參數,但是python的約定是這倆,還是不要改的好.
  • 110道Python面試題
    python程序的時候會霸佔python解釋器(加了一把鎖即GIL),使該進程內的其他線程無法運行,等該線程運行完後其他線程才能運行。如果線程運行過程中遇到耗時操作,則解釋器鎖解開,使其他線程運行。所以在多線程中,線程的運行仍是有先後順序的,並不是同時進行。
  • c#.net多線程編程教學(3):線程同步
    正在閱讀:c#.net多線程編程教學(3):線程同步c#.net多線程編程教學(3):線程同步2005-07-07 10:44出處:作者:c-sharpcorner>隨著對多線程學習的深入,你可能覺得需要了解一些有關線程共享資源的問題. .NET framework提供了很多的類和數據類型來控制對共享資源的訪問。
  • 110 道 Python 面試筆試題
    5、談下python的GIL    GIL 是python的全局解釋器鎖,同一進程中假如有多個線程運行,一個線程在運行python程序的時候會霸佔python解釋器(加了一把鎖即GIL),使該進程內的其他線程無法運行
  • 10個Python爬蟲入門實例
    爬蟲,準備了幾個簡單的入門實例,分享給大家。涉及主要知識點:web是如何交互的requests庫的get、post函數的應用response對象的相關函數,屬性python文件的打開,保存代碼中給出了注釋,並且可以直接運行哦如何安裝requests庫(安裝好python的朋友可以直接參考,沒有的,
  • 高級分享:Java多線程你真的理解透徹了嗎?帶你玩轉一次多線程!
    不知道怎麼引入正文相信後端同學在開發的時候多多少少都會涉及到多線程開發,作為Java開發的我也同樣會經常用到多線程開發。我認為Java語言在處理多線程上是非常優秀的,我們可以使用簡明的代碼實現線程的創建、啟動、管理等。
  • Python入門系列(十二)——GUI+多進程
    不好意思,拖堂了,此為終章,補充蠻重要的兩點一、GUI二、多進程一、GUI話說,python做圖形界面並不明智