翻譯:老齊算法
譯者注:與本文相關圖書推薦:《Python大學實用教程》《跟老齊學Python:輕鬆入門》編程
本文將分兩部分刊發。bash
Python線程容許程序的不一樣部分同時運行,並能夠簡化設計。若是你對Python有一些經驗,而且但願使用線程爲程序加速,那麼本文就是爲你準備的!微信
線程是一個獨立的流,這意味着你的程序能夠同時作兩件事,可是,對於大多數Python程序,不一樣的線程實際上並不一樣時執行,它們只是看起來像是同時執行。多線程
人們很容易認爲線程是在程序上運行兩個(或更多)不一樣的處理器,每一個處理器同時執行一個獨立的任務。這種見解大體正確,線程可能在不一樣的處理器上運行,但一個處理器一次只能運行一個線程。架構
要同時運行多個任務,不能用Python的標準方式實現,能夠用不一樣的編程語言,或者多個進程實現,這樣作的開發成本就高了。併發
因爲用CPython實現了Python業務,線程可能不會加速全部任務,這是GIL(全稱Global Interpreter Lock)的緣由,一次只能運行一個Python線程。app
若是某項任務須要花費大量時間等待外部事件,那麼就能夠應用多線程。若是是須要對CPU佔用高而且花費不多時間等待外部事件,多線程可能枉費。編程語言
對於用Python編寫並在標準CPython實現上運行的代碼,這是正確的。若是你的線程是用C編寫的,那麼它們就可以釋放GIL、併發運行。若是你在不一樣的Python實現上運行,也能夠查看文檔,瞭解它如何處理線程。ide
若是你正在運行一個標準的Python程序,只使用Python編寫,而且有一個CPU受限的問題,那麼你應該用多進程解決此問題。
將程序架構爲使用線程也能夠提升設計的清晰度。你將在下文中學習的大多數示例不必定會運行得更快,由於它們使用線程。在這些示例中使用線程有助於使設計更清晰、更易於推理。
因此,讓咱們中止談論線程並開始使用它!
如今你已經知道了什麼是線程,讓咱們來學習如何製做線程。Python標準庫提供了線程模塊threading
,它包含了你將在本文中看到的大部份內容。在這個模塊中,Thread
是對線程的封裝,提供了簡單的實現接口。
要建立一個線程,須要建立Thread
的實例,而後調用它的.start()
方法:
import logging
import threading
import time
def thread_function(name):
logging.info("Thread %s: starting", name)
time.sleep(2)
logging.info("Thread %s: finishing", name)
if __name__ == "__main__":
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%H:%M:%S")
logging.info("Main : before creating thread")
x = threading.Thread(target=thread_function, args=(1,))
logging.info("Main : before running thread")
x.start()
logging.info("Main : wait for the thread to finish")
# x.join()
logging.info("Main : all done")
複製代碼
若是你查看日誌,能夠看到在main
部分正在建立和啓動線程:
x = threading.Thread(target=thread_function, args=(1,))
x.start()
複製代碼
用函數thread_function()
和arg(1,)
建立一個Thread
實例。在本文中用整數做爲線程的名稱,threading.get_ident()
能夠返回線程的名稱,但可讀性較差。
thread_function()
函數的做用不大,它只是記錄一些日誌消息,在這些消息之間加上time.sleep()
。
當你執行此程序時,輸出將以下所示:
$ ./single_thread.py
Main : before creating thread
Main : before running thread
Thread 1: starting
Main : wait for the thread to finish
Main : all done
Thread 1: finishing
複製代碼
你會注意到代碼的main
部分結束以後,Thread
才結束。後面會揭示這麼作的緣由。
在計算機科學中,daemon
是在後臺運行的程序。
Python的threading
模塊對daemon
有更具體的含義。當程序退出時,守護線程會當即關閉。考慮這些定義的一種方法是將daemon
視爲在後臺運行的線程,而沒必要擔憂關閉它。
若是程序中正在執行的Threads
不是daemons
,則程序將在終止以前等待這些線程完成。然而,若是Threads
是daemons
,當程序退出時,它們就終止了。
讓咱們更仔細地看看上面程序的輸出,最後兩行是有點意思的。當運行這個程序時,在__main__
打印完all done
後以及線程結束以前會暫停大約2秒。
這個暫停是Python等待非後臺線程完成。當Python程序結束時,關閉操做是清除線程中的程序。
若是查看threading
模塊的源代碼,你將看到threading._shutdown()
方法,它會遍歷全部正在運行的線程,並在每個沒有設置daemon
標誌的線程上調用.join()
方法。
所以,程序在退出時會等待,由於線程自己正在sleep(time.sleep(2)
)中。一旦完成並打印了消息,.join()
將返回,程序才能夠退出。
一般,這是你想要的,可是咱們還有其餘的選擇。讓咱們首先使用一個daemon
線程來重複這個程序。你能夠修改Thread
實例化時的參數,添加daemon=True
:
x = threading.Thread(target=thread_function, args=(1,), daemon=True)
複製代碼
如今運行程序時,應看到如下輸出:
$ ./daemon_thread.py
Main : before creating thread
Main : before running thread
Thread 1: starting
Main : wait for the thread to finish
Main : all done
複製代碼
與前面不一樣的是,前面所輸出的最後一行在這裏沒有了。thread_function()
沒有執行完,它是一個daemon
線程,因此當_main__
執行到達它的末尾時,程序結束,後臺線程也就結束了。
.join()
方法守護線程很方便,可是,若是要實現線程徹底執行,而不是被迫退出,應該怎麼辦?如今讓咱們回到原始程序,看看註釋掉的那一行:
# x.join()
複製代碼
要讓一個線程等待另外一個線程完成,能夠調用.join()
。取消對該行的註釋,主線程將暫停並等待線程x
,直到它運行結束。
你是否在程序中用守護線程或普通線程測試了這個問題?這並不重要。若是執行某個線程的.join()
方法,該語句將一直等待,直到每一個線程都完成。
到目前爲止,示例代碼只使用了兩個線程:一個是主線程,另外一個是以threading.Thread
對象開始的線程。
一般,您會但願啓動更多線程並讓它們作一些有趣的工做。咱們先來看看比複雜的方法,而後再看比較簡單的方法。
啓動多線程比較複雜的方法是你已經知道的:
import logging
import threading
import time
def thread_function(name):
logging.info("Thread %s: starting", name)
time.sleep(2)
logging.info("Thread %s: finishing", name)
if __name__ == "__main__":
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%H:%M:%S")
threads = list()
for index in range(3):
logging.info("Main : create and start thread %d.", index)
x = threading.Thread(target=thread_function, args=(index,))
threads.append(x)
x.start()
for index, thread in enumerate(threads):
logging.info("Main : before joining thread %d.", index)
thread.join()
logging.info("Main : thread %d done", index)
複製代碼
這段代碼使用與上面看到的相同機制來啓動線程,建立一個Thread
實例對象,而後調用.start()
。程序中生成一個由Thread
實例組成的列表,後面再調用每一個實例.join()
方法。
屢次運行此代碼可能會產生一些有趣的結果。下面是個人機器的輸出示例:
$ ./multiple_threads.py
Main : create and start thread 0.
Thread 0: starting
Main : create and start thread 1.
Thread 1: starting
Main : create and start thread 2.
Thread 2: starting
Main : before joining thread 0.
Thread 2: finishing
Thread 1: finishing
Thread 0: finishing
Main : thread 0 done
Main : before joining thread 1.
Main : thread 1 done
Main : before joining thread 2.
Main : thread 2 done
複製代碼
若是仔細檢查輸出,你將看到全部三個線程都按照你可能指望的順序開始,但在本例中,它們是按照相反的順序完成的!屢次運行將產生不一樣的排序,能夠經過查找Thread x: finishing
消息來了解每一個線程什麼時候完成。
線程的運行順序由操做系統決定,很難預測,它可能(並且極可能)因運行而異,所以在設計使用線程的算法時須要注意這一點。
幸運的是,Python提供了幾個模塊,你稍後將看到這些模塊用來幫助協調線程並使它們一塊兒運行。在此以前,讓咱們看看如何更簡單地管理一組線程。
有一種比上面看到的更容易啓動多線程的方法,它被稱爲ThreadPoolExecutor
,是標準庫中的concurrent.futures
的一員(從Python3.2開始)。
建立它的最簡單方法是使用上下文管理器的with
語句,用它實現對線程池的建立和銷燬。
下面是爲了使用ThreadPoolExecutor
而重寫的上一個示例中的__main__
部分代碼:
import concurrent.futures
# [rest of code]
if __name__ == "__main__":
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%H:%M:%S")
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
executor.map(thread_function, range(3))
複製代碼
代碼建立了一個ThreadPoolExecutor
做爲上下文管理器,告訴它須要在線程池中有多少個工做線程。而後它使用.map()
遍歷可迭代對象,在上面的例子中是range(3)
,將每一個可迭代對象傳遞給線程池中的一個線程。
with
語句塊的尾部,默認會調用ThreadPoolExecutor
的每一個線程的.join()
方法,建議你儘量使用ThreadPoolExecutor
做爲上下文管理器,這樣你就永遠不會忘記對執行線程.join()
。
注意:使用ThreadPoolExecutor
可能會致使一些混亂的錯誤。
例如,若是調用不帶參數的函數,但在.map()
中傳了參數,則線程應當拋出異常。
不幸的是,ThreadPoolExecutor
隱藏了該異常,而且(在上面的狀況下)程序將在沒有輸出的狀況下終止。一開始調試可能會很混亂。
運行正確的示例代碼將生成以下輸出:
$ ./executor.py
Thread 0: starting
Thread 1: starting
Thread 2: starting
Thread 1: finishing
Thread 0: finishing
Thread 2: finishing
複製代碼
一樣,請注意Thread 1
是在Thread 0
以前完成的,線程執行順序的調度是由操做系統完成的,所遵循的計劃也不易理解。
(未完待續)
關注微信公衆號:老齊教室。讀深度文章,得精湛技藝,享絢麗人生。