Python 標準庫簡介concurrent.futures

1 模塊簡介

concurrent.futures模塊是在Python3.2中添加的。根據Python的官方文檔,concurrent.futures模塊提供給開發者一個執行異步調用的高級接口。concurrent.futures基本上就是在Python的threading和multiprocessing模塊之上構建的抽象層,更易於使用。儘管這個抽象層簡化了這些模塊的使用,但是也降低了很多靈活性,所以如果你需要處理一些定製化的任務,concurrent.futures或許並不適合你。

concurrent.futures包括抽象類Executor,它並不能直接被使用,所以你需要使用它的兩個子類:ThreadPoolExecutor或者ProcessPoolExecutor。正如你所猜的,這兩個子類分別對應着Python的threading和multiprocessing接口。這兩個子類都提供了池,你可以將線程或者進程放入其中。

在計算機科學中,future有着特殊的含義。當使用concurrent技術時,它可以被用於同步操作。future也可以描述在任務結束之前,進程或者線程的結果。我喜歡將它看作即將發生的結果。

2 模塊使用

2.1 創建池

你可以通過concurrent.futures很容易地創建一個工作池。實例如下,

import os
import urllib.request

from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import as_completed

def downloader(url):
    req = urllib.request.urlopen(url)
    filename = os.path.basename(url)
    ext = os.path.splitext(url)[1]
    if not ext:
        raise RuntimeError("URL does not contain an extension")

    with open(filename,"wb") as file_handle:
        while True:
            chunk = req.read(1024)
            if not chunk:
                break
            file_handle.write(chunk)
        msg = "Finished downloading {filename}".format(filename = filename)
        return msg

def main(urls):
    with ThreadPoolExecutor(max_workers = 5) as executor:
        futures = [executor.submit(downloader,url) for url in urls]
        for future in as_completed(futures):
            print(future.result())

if __name__ == "__main__":
    urls = ["http://www.irs.gov/pub/irs-pdf/f1040.pdf",
            "http://www.irs.gov/pub/irs-pdf/f1040a.pdf",
            "http://www.irs.gov/pub/irs-pdf/f1040ez.pdf",
            "http://www.irs.gov/pub/irs-pdf/f1040es.pdf",
            "http://www.irs.gov/pub/irs-pdf/f1040sb.pdf"]
    main(urls)

首先,我們引入我們需要的模塊。然後,我們創建downloader函數,會檢查URL是否有擴展名,如果沒有擴展名,我們就會拋出RuntimeError錯誤。然後,我們創建main函數,在這裏,我們會實例化一個線程池。你可以在ThreadPoolExecutor和ProcessPoolExecutor上使用Python的with語句。

我們設置線程池中工作線程數目爲5。然後我們通過列表創建一組futures(或者爲任務),最後,我們調用as_complete函數。這個函數是一個迭代器,當任務結束時,會返回任務。當它們完成時,我們將結果打印出來,結果就是我們的downloader函數返回的一個字符串。

如果我們使用的函數是計算密集型的,我們可以使用ProcessPoolExecutor,替代ThreadPoolExecutor,僅僅需要修改一行代碼。

我們可以使用concurrent.futures中的map方法,讓代碼更加簡潔。讓我們將上述代碼重構一下,如下所示,

import os
import urllib.request

from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import as_completed

def downloader(url):
    req = urllib.request.urlopen(url)
    filename = os.path.basename(url)
    ext = os.path.splitext(url)[1]
    if not ext:
        raise RuntimeError("URL does not contain an extension")

    with open(filename,"wb") as file_handle:
        while True:
            chunk = req.read(1024)
            if not chunk:
                break
            file_handle.write(chunk)
        msg = "Finished downloading {filename}".format(filename = filename)
        return msg

def main(urls):
    with ThreadPoolExecutor(max_workers = 5) as executor:
        return executor.map(downloader, urls ,timeout = 60)

if __name__ == "__main__":
    urls = ["http://www.irs.gov/pub/irs-pdf/f1040.pdf",
            "http://www.irs.gov/pub/irs-pdf/f1040a.pdf",
            "http://www.irs.gov/pub/irs-pdf/f1040ez.pdf",
            "http://www.irs.gov/pub/irs-pdf/f1040es.pdf",
            "http://www.irs.gov/pub/irs-pdf/f1040sb.pdf"]
    results = main(urls)
    for result in results:
        print(result)

主要的區別在main函數中,已經被減少到兩行代碼。concurrent.futures中的map方法類似於Python中的map方法,它獲取一個函數和一個可迭代對象,然後在可迭代對象上每個元素上依次調用這個函數。你也可以在你的每個線程中加入timeout,如果某一個線程掛掉,整個程序就會停止。最後,在Python3.5中,他們添加了chunksize變量,在很大的可迭代對象上使用線程池,可以改善性能。如果你使用的進程池,chunksize不會起作用。

2.2 死鎖

concurrent.futures模塊有一個缺陷,當調用一個關聯任務,這個任務又在等待另一個任務時,你就會進入死鎖。這個聽起來很令人困惑,讓我們看一個實例,

from concurrent.futures import ThreadPoolExecutor

def wait_forever():
    my_future = executor.submit(zip,[1,2,3],[4,5,6])
    result = my_future.result()
    print(result)

if __name__ == "__main__":
    executor = ThreadPoolExecutor(max_workers = 1)
    executor.submit(wait_forever)

首先,我們引入ThreadPoolExecutor類,並實例化它。需要注意的是,我們設置最大的工作進程數目爲1。然後,我們註冊wait_forever函數。在wait_forever函數中,我們向線程池中註冊了另一個任務--將兩個列表打包在一起,獲得這個操作的結果,並將結果打印出出來。但是,我們卻創建了一個死鎖。原因就是我們有一個任務等待另一個任務結束,也就是我們希望一個未完成的操作去等待另一個未完成的無效操作。

讓我們將它重寫,如下所示,

from concurrent.futures import ThreadPoolExecutor

def wait_forever():
    my_future = executor.submit(zip,[1,2,3],[4,5,6])
    return my_future

if __name__ == "__main__":
    executor = ThreadPoolExecutor(max_workers = 3)
    fut = executor.submit(wait_forever)
    result = fut.result()
    print(list(result.result()))

在這裏,我們僅僅返回函數內部的任務,然後獲取它的結果。在我們返回的任務上調用result()方法的結果就會包含我們想要的結果,看起來似乎有些令人困惑。無論如何,如果我們在這個任務上調用result()方法,我們就會獲得一個打包的對象,爲了瞭解實際的結果就是是什麼,我們使用Python的list函數將打包對象進行包裹,然後打印出來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章