15. Python 程序運行速度如何提高十倍?第一遍滾雪球學 Python 收工
如果你有想要交流的想法、技術,歡迎在評論區留言。
本篇文章將給大家介紹 Python 多線程與多進程相關知識,學習完該知識點之后,你的 Python 程序將進入另一個高峰。
十五、Python 多線程與多進程
先嘗試理解線程與進程的概念,進程范圍大,一個進程可能會包含多個線程,OK,了解到這一步就可以了,知道誰包含誰已經很不錯了,細節的地方慢慢研究。
打開你電腦上的任務管理器,注意這里面以前說的叫做殺掉進程。
15.1 Python 多線程
讓我們把視角轉換一下,先從進程中抽離出來,看一下線程,在學習這部分內容的時候,這兩個概念一定不要弄錯,弄錯就翻車了。
15.1.1 簡單的多線程
如果一個線程只完成一個事情,那程序會變得特別呆板,例如現在你正在給編寫一段代碼,那你在編寫代碼的過程中,你使用的 IDE(代碼編輯器)就完全不能做其它事情了,必須等到編寫完所有代碼之后才可以執行其它操作,所有的事情只能一件挨著一件的做。而且在這個線程會將資源霸占住,例如讓其操作一個文件,必須等到它完成操作其它程序才可以使用,這叫做單線程。
如何實現多線程呢,通過導入 Python 內置的 threading 模塊可以解決該問題。
import threading # 定義一個函數,在線程中運行 def thread_work(): pass # 在 Python 中運行線程 # 建立線程對象 my_thread = threading.Thread(target=thread_work) # 啟動線程 my_thread.start()
建立一個線程使用的是 threading 模塊中的 Thread 方法,該方法會創建一個 Thread 對象(線程對象),使用該方法的時候需要注意方法的參數值是一個函數名稱,該參數為 target,后面是線程要調用的函數名稱,沒有小括號。返回的線程對象在上述代碼中叫做 my_thread,自己定義的任意名稱都是可以的,遵循變量命名規則即可。
線程的啟動需要調用線程對象的 start 方法。
import threading import time # 定義一個函數,在線程中運行 def thread_work(): # 函數內部方法 print(" my_thread 線程開始工作") time.sleep(10) # 暫停十秒,為了方便模擬操作 print("時間到了,線程繼續工作") print("主線程開始運行") # 在 Python 中運行線程 # 建立線程對象 my_thread = threading.Thread(target=thread_work) # 啟動線程 my_thread.start() time.sleep(1) # 主線程停止 1 秒 print("主線程結束")
代碼運行之后重點注意輸出的順序。
主線程開始運行 my_thread 線程開始工作 主線程結束 時間到了,線程繼續工作
主線程結束 輸出之后,需要等待幾秒鐘的時間,我們定義的子線程才會開始運行,即輸出 時間到了,線程繼續工作。
15.1.2 子線程傳遞參數
在創建線程的時候,除了直接調用某函數,也可以向子線程中的函數里傳遞參數,具體語法格式如下:
my_thread = threading.Thread(target=函數名稱,args=['參數1','參數2',....])
具體案例如下,像 thread_work 函數中傳遞一個 橡皮擦。
import threading import time # 定義一個函數,在線程中運行 def thread_work(name): # 函數內部方法 print(" my_thread 線程開始工作") print("我是從主線程傳遞進來的參數:", name) time.sleep(10) # 暫停十秒,為了方便模擬操作 print("時間到了,線程繼續工作") print("主線程開始運行") # 在 Python 中運行線程 # 建立線程對象 my_thread = threading.Thread(target=thread_work, args=["橡皮擦"]) # 啟動線程 my_thread.start() time.sleep(1) # 主線程停止 1 秒 print("主線程結束")
參數在傳遞的時候,需要與函數定義時參數匹配。多線程中不建議使用相同的變量,很容易出現問題,建議每個線程使用自己的局部變量,互相之間不要產生干擾。
15.1.3 線程命名
每個線程在啟動之后,如果沒有手動命名,系統會自動給其命名為 Thread-n,在程序中可以使用 currentThread().getName() 獲取線程的名稱。隨著 Python 版本的迭代,currentThread 方法已經逐步被 current_thread 替代。
import threading import time # 定義一個函數,在線程中運行 def thread_work1(name): # 函數內部方法 print(threading.currentThread().getName()," 線程啟動") time.sleep(2) print(threading.currentThread().getName()," 線程啟動") # 定義一個函數,在線程中運行 def thread_work2(name): # 函數內部方法 print(threading.currentThread().getName(), " 線程啟動") time.sleep(2) print(threading.currentThread().getName(), " 線程啟動") print("主線程開始運行") # 在 Python 中運行線程 # 建立線程對象 my_thread1 = threading.Thread(target=thread_work1, args=["橡皮擦"]) my_thread2 = threading.Thread(target=thread_work2, args=["橡皮擦"]) # 啟動線程 my_thread1.start() # 啟動線程 my_thread2.start() time.sleep(1) # 主線程停止 1 秒 print("主線程結束")
代碼運行結果如下,可以重點看一下線程默認的名稱。
主線程開始運行 Thread-1 線程啟動 Thread-2 線程啟動 主線程結束 Thread-2 線程啟動 Thread-1 線程啟動
如果想要給線程起一個獨特的名字,可以在通過 Thread 方法建立線程時,使用參數 name = "線程名稱",該名稱就是為線程單獨命名。
import threading import time # 定義一個函數,在線程中運行 def thread_work1(name): # 函數內部方法 print(threading.currentThread().getName()," 線程啟動") time.sleep(2) print(threading.currentThread().getName()," 線程啟動") # 定義一個函數,在線程中運行 def thread_work2(name): # 函數內部方法 print(threading.currentThread().getName(), " 線程啟動") time.sleep(2) print(threading.currentThread().getName(), " 線程啟動") print("主線程開始運行") # 在 Python 中運行線程 # 建立線程對象 my_thread1 = threading.Thread(name="我是線程1(不建議用中文)",target=thread_work1, args=["橡皮擦"]) my_thread2 = threading.Thread(name="work thread",target=thread_work2, args=["橡皮擦"]) # 啟動線程 my_thread1.start() # 啟動線程 my_thread2.start() time.sleep(1) # 主線程停止 1 秒 print("主線程結束")
除了上述辦法以外,還可以使用 currentThread().setName() 給函數命名,自己可以嘗試下哦~
15.1.4 Daemon 守護線程
默認創建的線程都不是 Daemon 線程,正常情況下,一個程序建立了主線程和子線程,那程序結束需要等待所有的線程工作結束,因為如果主線程先結束了,那子線程會因為沒有可用資源而導致程序崩潰。
如果我們希望主線程結束了,子線程自行終止,那這時就要設置一下 Daemon 線程的屬性了,設置之后,主線程若是想要結束運行,需要檢查一下 Daemon 線程的屬性。
如果 Daemon 線程的屬性是 True,其它非 Daemon 線程執行結束,不會等待 Daemon 線程,主線程會自動結束。
如果 Daemon 線程屬性是 False,那主線程必須等待 Daemon 線程結束才會將程序結束運行。
以上內容翻譯成大白話就是可以把一個線程設置為 Daemon 線程,而且還可以設置一個屬性,如果屬性設置為 True,那該線程就不受重視了,其它線程結束,它就被結束了,如果設置為 False,那它就是最重要的了,主線程需要等著它結束運行,才可以進行下一步操作。
import threading import time # 定義一個函數,在線程中運行 def thread_work1(): # 函數內部方法 print(threading.currentThread().getName()," 線程啟動") # 等待 5 秒,如果被重視,那主線程將等待,如果不被重視,很快就會執行完畢 time.sleep(5) print(threading.currentThread().getName()," 線程啟動") # 定義一個函數,在線程中運行 def thread_work2(): # 函數內部方法 print(threading.currentThread().getName(), " 線程啟動") print(threading.currentThread().getName(), " 線程啟動") print("主線程開始運行") # 在 Python 中運行線程 # 建立線程對象 my_thread1 = threading.Thread(name="我是守護線程 Daemon",target=thread_work1) my_thread1.setDaemon(True) # 先設置為 True,該線程將不被重視 my_thread2 = threading.Thread(name="work thread",target=thread_work2) # 啟動線程 my_thread1.start() # 啟動線程 my_thread2.start() print("主線程結束")
以上代碼運行之后發現瞬間執行完畢了,并沒有等待 5 秒鐘,充分證明了不被重視的線程的處境。
接下來修改一個屬性,可以再看一下效果。
my_thread1.setDaemon(False)
運行之后發現程序等待 5 秒之后才結束運行,你是否發現了其中的差異呢?
15.1.5 堵塞主線程
主線程在工作的時候,如果希望子線程先運行,直到該子線程運行結束,主線程才繼續工作。
import threading import time # 定義一個函數,在線程中運行 def thread_work1(): # 函數內部方法 print(threading.currentThread().getName()," 線程啟動") time.sleep(5) print(threading.currentThread().getName()," 線程啟動") print("主線程開始運行") # 在 Python 中運行線程 # 建立線程對象 my_thread1 = threading.Thread(name="work thread",target=thread_work1) # 啟動線程 my_thread1.start() print("join 開始......") my_thread1.join() # 等待 work thead 線程運行結束 print("join 結束....") print("主線程結束")
join 方法可以增加一個參數,該參數表示等待的秒數,當秒數到了,主線程恢復工作。
my_thread.join(3) # 子線程運行 3 秒。
15.1.6 is_alive 檢驗子線程是否在工作
使用 join 方法之后,一般在后面需要加上一個 is_alive 方法,該方法會簡稱子線程是否工作結束了,如果子線程結束則返回 False,仍在工作則會返回 True。
import threading import time # 定義一個函數,在線程中運行 def thread_work1(): # 函數內部方法 print(threading.currentThread().getName()," 線程啟動") time.sleep(5) print(threading.currentThread().getName()," 線程啟動") print("主線程開始運行") # 在 Python 中運行線程 # 建立線程對象 my_thread1 = threading.Thread(name="work thread",target=thread_work1) # 啟動線程 my_thread1.start() print("join 開始......") my_thread1.join(2) # 等待 work thead 線程運行結束 print("join 結束....") print("子線程是否仍在工作?",my_thread1.is_alive()) time.sleep(3) print("子線程是否仍在工作?",my_thread1.is_alive()) print("主線程結束")
有的教程或者書籍中還會使用 isAlive 方法來進行判斷,這是因為 Python 版本的問題,后續建議使用 is_alive 方法。
15.1.7 自定義線程類
threading.Thread 是 threading 模塊內的一個類,我們可以繼承這個類,定義自己的線程類,定義的時候有兩個需要注意的地方,第一個需要在構造函數中調用 threading.Thread.__init()__ 方法,第二個是需要在類內容定義好 run 方法。
之前的內容中,通過 threading.Thread 聲明一個線程對象時,執行 start 方法可以建立一個線程,start 方法就是在調用類中的 run 方法。
import threading class MyThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): print(threading.Thread.getName(self)) print("橡皮擦定義好的線程") my_thread = MyThread() my_thread.run() you_thread = MyThread() you_thread.run()
15.1.8 資源鎖定與解鎖
在多線程程序中經常碰到多個線程使用一個共享資源的情況,為了確保共享資源在多線程共享時不出現問題,需要使用 theading.Lock 對象的兩個方法 acquire 與 release 。
import threading my_num = 0 lock = threading.Lock() class MyThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): print(threading.Thread.getName(self)) # 調用全局變量 global my_num my_num += 10 print("現在的數字是:", my_num, "\n") # 線程列表 ts = [] # 批量創建 10 個線程 for i in range(0, 10): my_thread = MyThread() ts.append(my_thread) # 啟動 10 個線程 for t in ts: t.start() # 等待所有線程結束 for t in ts: t.join()
以上代碼沒有使用 acquire 與 release 方法,出現的結果無規律可循,是因為各線程無法預期誰會優先取得資源,專業描述叫做 線程以不可預知的速度向前推進,當然有的地方叫做線程競速,一個意思。
稍微修改一下就可以讓線程按照規矩執行了,在使用全局變量的時候,先鎖定資源,使用之后在釋放資源。
# 調用全局變量 global my_num lock.acquire() my_num += 10 lock.release() print("現在的數字是:", my_num, "\n")
以上內容如果使用 acquire 連續使用兩次就會導致死鎖。
關于死鎖問題與資源鎖定 Threading.RLock,還有高級鎖定相關的知識,在以后的滾雪球中繼續學習,先階段掌握基本的鎖定就可以啦。
15.1.9 未來要學習的知識
進展到現在你已經可以實現簡單的多線程開發了,但是對于線程類的學習只揭示了最簡單的一部分,后續我們將學習到如下內容,都在第二遍滾雪球時學習。
queue 模塊,也叫做隊列模塊
Semaphore 信號量,高級鎖機制
Barrier 柵欄
Event 線程通訊機制
15.2 subprocess 模塊
subprocess 是 Python 中用于建立子進程的模塊,注意是子進程。導入該模塊使用 import subprocess。
15.2.1 Popen 方法
該方法可以打開計算機內部的應用程序,也可以打開自己寫好的程序,文件路徑寫對即可。
import subprocess # 打開計算機 calc_pro = subprocess.Popen('calc.exe') # 打開畫板 mspaint_pro = subprocess.Popen('mspaint.exe')
打開的子進程,主程序已經結束了。
15.2.2 Popen 方法攜帶參數
可以在 Popen 方法打開程序的時候,傳遞一個參數進去,該參數為列表類型,第一個元素是要打開的應用程序,第二個則是傳遞進去的文件。
例如打開畫圖程序。
import subprocess # 打開計算機 # calc_pro = subprocess.Popen('calc.exe') # 打開畫板 mspaint_pro = subprocess.Popen(['mspaint.exe','./pic.jpg'])
文件的路徑不要寫錯,以上代碼會打開畫板程序并且在畫板打開一個圖片。
15.2.3 通過 start 打開程序
在電腦上通過雙擊就可以打開某種文件,這是因為 Windows 系統已經給我們做好了關聯,那能不能在 Python 中也模擬出該方式呢,很簡單,通過 subprocess.Popen 方法的參數即可實現。
import subprocess # 打開圖片 mspaint_pro = subprocess.Popen(['start','./pic.jpg'],shell = True)
使用該代碼打開圖片是使用你默認的圖片預覽程序,滿足了剛才所說的場景。該方法核心使用的有兩個地方一個是原程序位置使用的是 start 關鍵字(僅在 Windows 上有效),第一個是 shell = True 參數。
15.2.4 通過 run 方法調用子進程
該方法屬于新增方法,通過 subprocess.run 方法即可調用子進程。具體內容可以自行嘗試即可。
15.3 這篇博客的總結
本篇博客主要內容是 Python 的多線程應用,順帶著說了一點點關于進程的相關知識,對于多線程,很多學習 Python 很久的同學都不一定可以搞清楚,在這里希望大家第一次學習先有概念支撐即可,能掌握多少在本階段不重要,學習是需要時間積累的,一遍就會那是天才或者是吹牛的,有很多工作 2~3 年的還不一定能把多線程多進程說清楚呢,所以不要著急哦,繼續往后面看,往后面學就好了。
第一遍滾雪球學 Python 收官。下期見。
博主 ID:夢想橡皮擦,希望大家
、
評論
、
。
Python 任務調度
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。