An Intro to Threading in Python – Real Python????????
1. 什么是線程?
????????線程是一個獨立的執行流程。這意味著您的程序將有兩件事情同時發生。但對于大多數 Python 3 實現來說,不同的線程實際上并不是同時執行的:它們只是看起來是這樣。
????????人們很容易把線程想象成程序上運行著兩個(或多個)不同的處理器,每個處理器同時執行一項獨立的任務。這幾乎是對的。線程可能運行在不同的處理器上,但它們一次只能運行一個。
????????要同時運行多個任務,需要使用非標準的 Python 實現,用不同的語言編寫部分代碼,或者使用多進程,而多進程會帶來一些額外的開銷。
????????由于 CPython 實現 Python 的工作方式,線程可能無法加快所有任務的速度。這是由于與 GIL 的交互作用,基本上每次只能運行一個 Python 線程。
????????大部分時間都在等待外部事件的任務一般都適合使用線程。而那些需要大量 CPU 計算且等待外部事件的時間很少的問題,運行速度可能根本不會快。
????????這適用于用 Python 編寫并在標準 CPython 實現上運行的代碼。如果您的線程是用 C 語言編寫的,它們可以釋放 GIL 并同時運行。如果您在不同的 Python 實現上運行,請查閱文檔,了解它是如何處理線程的。
????????如果您運行的是標準 Python 實現,只用 Python 編寫,并且有 CPU 限制的問題,那么您應該選擇多處理模塊。
????????將程序架構為使用線程也能提高設計的清晰度。您將在本教程中學到的大多數示例并不一定會因為使用了線程而運行得更快。在這些示例中使用線程有助于使設計更簡潔、更易于推理。
????????所以,讓我們停止談論線程,開始使用它吧!
?2. 開始線程
????????現在您已經知道了線程是什么,讓我們來學習如何創建一個線程。Python 標準庫提供了線程,它包含了你在本文中將看到的大多數基元。本模塊中的 Thread 很好地封裝了線程,提供了一個簡潔的接口來使用它們。
????????要啟動一個單獨的線程,需要創建一個線程實例,然后告訴它 .start():
import loggingimport threadingimport timedef 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")
????????如果查看一下日志語句,就會發現主要部分是創建和啟動線程:
x = threading.Thread(target=thread_function, args=(1,))x.start()
????????創建線程時,你會向它傳遞一個函數和一個包含該函數參數的列表。在本例中,你告訴線程運行 thread_function(),并將 1 作為參數傳遞給它。
????????本文將使用順序整數作為線程的名稱。threading.get_ident()可以為每個線程返回一個唯一的名稱,但這些名稱通常既不簡短也不易讀。
????????thread_function() 本身并沒有做什么。它只是簡單地記錄一些信息,并在信息之間加入 time.sleep()。
????????運行該程序(注釋掉第 20 行)時,輸出結果如下:
$ ./single_thread.pyMain ???: before creating threadMain ???: before running threadThread 1: startingMain ???: wait for the thread to finishMain ???: all doneThread 1: finishing
????????你會注意到,線程在代碼的 Main 部分結束后才結束。在下一節中,我們將回過頭來討論為什么會這樣,并談談神秘的第 20 行。
3. 守護進程線程
????????在計算機科學中,守護進程是在后臺運行的進程。
????????Python 線程對守護進程有更具體的含義。守護線程會在程序退出時立即關閉。思考這些定義的一種方法是,將守護線程視為一個在后臺運行的線程,而不必擔心關閉它。
????????如果程序運行的線程不是守護進程,那么程序會等待這些線程完成后才終止。而作為守護進程的線程則會在程序退出時被殺死。
????????讓我們再仔細看看上面程序的輸出結果。最后兩行是最有趣的部分。當你運行程序時,你會發現在 __main__ 打印出 all done 消息后,在線程結束前有一個停頓(約 2 秒)。
????????這個停頓是 Python 在等待非調用線程完成。當 Python 程序結束時,關閉過程的一部分就是清理線程例程。
????????如果您查看 Python 線程的源代碼,就會發現 threading._shutdown() 會遍歷所有正在運行的線程,并對每一個沒有設置守護進程標志的線程調用 .join()。
????????因此,程序等待退出是因為線程本身處于休眠狀態。一旦完成并打印了信息,.join() 就會返回,程序就可以退出了。
????????通常,這種行為是你想要的,但我們還有其他選擇。首先,讓我們用守護進程線程重復該程序。方法是改變線程的構造,添加 daemon=True 標志:
x = threading.Thread(target=thread_function, args=(1,), daemon=True)
????????現在運行程序,應該會看到以下輸出:
$ ./daemon_thread.pyMain ???: before creating threadMain ???: before running threadThread 1: startingMain ???: wait for the thread to finishMain ???: all done
????????這里的區別在于輸出的最后一行不見了。 thread_function() 沒有機會完成。它是一個守護線程,所以當 __main__ 的代碼結束,程序想要結束時,守護線程就被殺死了。
4. 線程的使用場景
????????線程非常適合處理以下類型的任務:
????????1. I/O 密集型任務:例如文件讀寫、網絡請求等。這類任務通常涉及大量的等待時間,線程可以在等待期間執行其他任務,從而提高效率。
????????2. 后臺任務:例如日志記錄、監控系統狀態、處理定時任務等。這類任務通常不需要立即的響應,可以在后臺異步處理。
5. 高級線程操作
5.1 使用線程池
????????在處理大量線程時,直接管理每個線程可能會變得復雜。此時,使用線程池(ThreadPoolExecutor)可以簡化管理。
from concurrent.futures import ThreadPoolExecutorimport loggingimport threadingimport timedef 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 pool")with ThreadPoolExecutor(max_workers=3) as executor:for i in range(5):executor.submit(thread_function, i)logging.info("Main ???: all done")
????????在這個例子中,ThreadPoolExecutor 創建了一個包含最多 3 個工作線程的線程池,并提交了 5 個任務。線程池會自動管理線程的生命周期和任務調度。
5.2 線程同步
????????在線程之間共享數據時,需要注意線程安全問題。Python 提供了多種同步機制,例如鎖(Lock)、信號量(Semaphore)和條件變量(Condition)。
????????鎖(Lock)
import threadinglock = threading.Lock()shared_resource = 0def thread_safe_increment():global shared_resourcewith lock:shared_resource += 1threads = []for _ in range(5):t = threading.Thread(target=thread_safe_increment)threads.append(t)t.start()for t in threads:t.join()print(shared_resource)
????????信號量(Semaphore)
????????信號量適用于需要限制同時訪問資源的線程數量的場景。
import threadingimport timesemaphore = threading.Semaphore(3)def access_resource(name):with semaphore:print(f"{name} is accessing the resource")time.sleep(2)print(f"{name} is releasing the resource")threads = []for i in range(5):t = threading.Thread(target=access_resource, args=(f"Thread-{i}",))threads.append(t)t.start()for t in threads:t.join()
5.3 線程間通信
????????Python 提供了隊列(Queue)來實現線程之間的安全通信。
import threadingimport queueimport timeq = queue.Queue()def producer():for i in range(5):print(f"Producing {i}")q.put(i)time.sleep(1)def consumer():while True:item = q.get()if item is None:breakprint(f"Consuming {item}")time.sleep(2)t1 = threading.Thread(target=producer)t2 = threading.Thread(target=consumer)t1.start()t2.start()t1.join()q.put(None)t2.join()
????????在這個例子中,生產者線程將數據放入隊列,而消費者線程從隊列中取出數據進行處理。
6. 多線程的注意事項
????????1. 避免死鎖:當多個線程互相等待對方釋放資源時,會導致程序卡死。
????????2. GIL 的限制:由于 Python 的全局解釋器鎖(GIL),多線程并不能真正并行執行 CPU 密集型任務。可以考慮使用多進程來繞過這個限制。
????????3. 資源清理:確保在程序結束時正確關閉和清理線程。
7. 結論
????????通過以上的介紹和示例,相信您已經對 Python 中的線程有了更深入的理解。線程在處理 I/O 密集型任務和后臺任務時非常有用,但在使用時需要注意線程同步和通信等問題,以避免線程安全問題。