【譯】線程:概念和應用(1)

翻譯:老齊算法

譯者注:與本文相關圖書推薦:《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,則程序將在終止以前等待這些線程完成。然而,若是Threadsdaemons,當程序退出時,它們就終止了。

讓咱們更仔細地看看上面程序的輸出,最後兩行是有點意思的。當運行這個程序時,在__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

有一種比上面看到的更容易啓動多線程的方法,它被稱爲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以前完成的,線程執行順序的調度是由操做系統完成的,所遵循的計劃也不易理解。

未完待續

關注微信公衆號:老齊教室。讀深度文章,得精湛技藝,享絢麗人生。

相關文章
相關標籤/搜索