該賞金過期5天。回答這個問題有資格獲得 50聲望獎勵。 Mortz想引起更多人對這個問題的關注。
根據我所閱讀的內容 - 例如這里- 我了解 I/O 操作釋放 GIL。所以,如果我必須讀取本地檔案系統上的大量檔案,我的理解是執行緒執行應該加快速度。
為了測驗這一點 - 我有一個檔案夾 ( input) 包含大約 10 萬個檔案 - 每個檔案只有一行和一個隨機整數。我有兩個功能 - 一個“順序”和一個“并發”,只是將所有數字相加
import glob
import concurrent.futures
ALL_FILES = glob.glob('./input/*.txt')
def extract_num_from_file(fname):
#time.sleep(0.1)
with open(fname, 'r') as f:
file_contents = int(f.read().strip())
return file_contents
def seq_sum_map_based():
return sum(map(extract_num_from_file, ALL_FILES))
def conc_sum_map_based():
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
return sum(executor.map(extract_num_from_file, ALL_FILES))
雖然這兩個函式給了我相同的結果 - “并發”版本慢了大約 3-4 倍。
In [2]: %timeit ss.seq_sum_map_based()
3.77 s ?± 50.2 ms per loop (mean ?± std. dev. of 7 runs, 1 loop each)
In [3]: %timeit ss.conc_sum_map_based()
12.8 s ?± 240 ms per loop (mean ?± std. dev. of 7 runs, 1 loop each)
我的代碼或我的理解有問題嗎?
uj5u.com熱心網友回復:
問題是雖然執行緒可能并行運行,但必須從硬碟驅動器順序讀取資料,因為只有單個讀取頭。然而,更糟糕的是,由于您已經并行化了 I/O 操作,底層作業系統將安排這些 I/O 任務,以便在切換到另一個執行緒之前僅部分處理這些檔案——畢竟,即使您只有單個整數,檔案頭仍然需要處理 - 導致讀取頭比嚴格順序代碼更瘋狂地跳躍。與簡單地按順序讀取每個檔案的全部內容相比,所有這些都導致了大量增加的開銷,這不需要那么多跳轉。
例如,如果您有一個執行緒從磁盤加載大量資料,而第二個執行緒對其執行一些時間密集型處理,則這不會是一個問題,因為這將允許時間密集型處理繼續不受 I/O 操作的阻塞。您的特定場景只是一個非常非常糟糕的案例,您放棄了 GIL 瓶頸以換取極其緩慢的 I/O 瓶頸。
簡而言之,您已經正確理解了 I/O 操作釋放 GIL,您只是得出了關于并行化檔案讀取的錯誤結論。
uj5u.com熱心網友回復:
另一個答案很好地說明了這一點:
您已經放棄了 GIL 瓶頸,以換取極其緩慢的 I/O 瓶頸。簡而言之,您已經正確理解了 I/O 操作釋放 GIL,您只是得出了關于并行化檔案讀取的錯誤結論。
我要補充的是執行緒檔案的閱讀可以是,如果你有I / O更好的性能,以備用,就像你可能有一個非常快的SSD。
使用這樣的實作:
class ThreadedFileReader:
def __init__(self, files, n=5):
self.files = deque(files)
self.threads = []
self.results = queue.Queue()
for _ in range(n):
t = threading.Thread(target=self.worker)
t.start()
self.threads.append(t)
def worker(self):
while self.files:
fname = self.files.pop()
with open(fname, encoding='utf-8') as f:
data = f.read()
self.results.put(len(data))
return
def join(self):
for t in self.threads:
t.join()
我在一個相對較快的 SSD(三星 970 EVO 1TB;沒有活動的輔助驅動器)上測驗了這個,讀取了大約 1000 個不同大小的檔案,平均 8800 個字符。在測驗中,我使用更多執行緒獲得了更好的性能……但收益遞減很快。
In [1]: len(ALL_FILES)
995
In [2]: %timeit ThreadedFileReader(ALL_FILES, n=1).join() # single threaded
61.8 ms ± 305 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [3]: %timeit ThreadedFileReader(ALL_FILES, n=2).join()
54.7 ms ± 158 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [4]: %timeit ThreadedFileReader(ALL_FILES, n=3).join()
56.1 ms ± 135 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [5]: %timeit ThreadedFileReader(ALL_FILES, n=4).join()
57.8 ms ± 131 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [6]: %timeit ThreadedFileReader(ALL_FILES, n=5).join()
58.9 ms ± 236 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [7]: %timeit ThreadedFileReader(ALL_FILES, n=50).join()
68.6 ms ± 378 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
因此,原則上您的想法是合理的,但前提是與順序讀取相比,您有足夠的 I/O 備用。除非您的存盤速度非常快,否則您不太可能需要超過一兩個額外的執行緒。如果您的存盤根本不快,單執行緒方法可能是要走的路。
請記住,如果您有多個執行緒同時讀取檔案,尤其是小檔案,則驅動器的隨機讀取功能可能會成為瓶頸。相比之下,大檔案的單執行緒方法可能會在接近驅動器的順序讀取能力時遇到瓶頸。根據硬體的不同,這些性能等級可能會有很大不同。
根據硬體和您正在讀取的資料的特性,順序讀取性能的好處可能超過并行讀取的任何潛在收益。
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/399894.html
上一篇:每個原子的記憶體順序是否正確?
