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函數將打包對象進行包裹,然後打印出來。