Wie lässt sich dieser Code am einfachsten parallelisieren?
Verwenden Sie einen PoolExecutor von concurrent.futures
. Vergleichen Sie den ursprünglichen Code mit diesem, Seite an Seite. Erstens ist die prägnanteste Art, dies zu tun, mit executor.map
:
...
with ProcessPoolExecutor() as executor:
for out1, out2, out3 in executor.map(calc_stuff, parameters):
...
oder aufgeschlüsselt, indem jede Aufforderung einzeln eingereicht wird:
...
with ThreadPoolExecutor() as executor:
futures = []
for parameter in parameters:
futures.append(executor.submit(calc_stuff, parameter))
for future in futures:
out1, out2, out3 = future.result() # this will block
...
Das Verlassen des Kontexts signalisiert dem Executor, dass er Ressourcen freigeben soll
Sie können Threads oder Prozesse verwenden und genau dieselbe Schnittstelle nutzen.
Ein praktisches Beispiel
Hier finden Sie einen funktionierenden Beispielcode, der den Wert von :
Geben Sie dies in eine Datei ein - futuretest.py:
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
from time import time
from http.client import HTTPSConnection
def processor_intensive(arg):
def fib(n): # recursive, processor intensive calculation (avoid n > 36)
return fib(n-1) + fib(n-2) if n > 1 else n
start = time()
result = fib(arg)
return time() - start, result
def io_bound(arg):
start = time()
con = HTTPSConnection(arg)
con.request('GET', '/')
result = con.getresponse().getcode()
return time() - start, result
def manager(PoolExecutor, calc_stuff):
if calc_stuff is io_bound:
inputs = ('python.org', 'stackoverflow.com', 'stackexchange.com',
'noaa.gov', 'parler.com', 'aaronhall.dev')
else:
inputs = range(25, 32)
timings, results = list(), list()
start = time()
with PoolExecutor() as executor:
for timing, result in executor.map(calc_stuff, inputs):
# put results into correct output list:
timings.append(timing), results.append(result)
finish = time()
print(f'{calc_stuff.__name__}, {PoolExecutor.__name__}')
print(f'wall time to execute: {finish-start}')
print(f'total of timings for each call: {sum(timings)}')
print(f'time saved by parallelizing: {sum(timings) - (finish-start)}')
print(dict(zip(inputs, results)), end = '\n\n')
def main():
for computation in (processor_intensive, io_bound):
for pool_executor in (ProcessPoolExecutor, ThreadPoolExecutor):
manager(pool_executor, calc_stuff=computation)
if __name__ == '__main__':
main()
Und hier ist die Ausgabe für einen Lauf von python -m futuretest
:
processor_intensive, ProcessPoolExecutor
wall time to execute: 0.7326343059539795
total of timings for each call: 1.8033506870269775
time saved by parallelizing: 1.070716381072998
{25: 75025, 26: 121393, 27: 196418, 28: 317811, 29: 514229, 30: 832040, 31: 1346269}
processor_intensive, ThreadPoolExecutor
wall time to execute: 1.190223217010498
total of timings for each call: 3.3561410903930664
time saved by parallelizing: 2.1659178733825684
{25: 75025, 26: 121393, 27: 196418, 28: 317811, 29: 514229, 30: 832040, 31: 1346269}
io_bound, ProcessPoolExecutor
wall time to execute: 0.533886194229126
total of timings for each call: 1.2977914810180664
time saved by parallelizing: 0.7639052867889404
{'python.org': 301, 'stackoverflow.com': 200, 'stackexchange.com': 200, 'noaa.gov': 301, 'parler.com': 200, 'aaronhall.dev': 200}
io_bound, ThreadPoolExecutor
wall time to execute: 0.38941240310668945
total of timings for each call: 1.6049387454986572
time saved by parallelizing: 1.2155263423919678
{'python.org': 301, 'stackoverflow.com': 200, 'stackexchange.com': 200, 'noaa.gov': 301, 'parler.com': 200, 'aaronhall.dev': 200}
Prozessorintensive Analyse
Wenn Sie in Python prozessorintensive Berechnungen durchführen, sollten Sie die ProcessPoolExecutor
leistungsfähiger zu sein als die ThreadPoolExecutor
.
Aufgrund der Global Interpreter Lock (auch bekannt als GIL) können Threads nicht mehrere Prozessoren verwenden, so dass die Zeit für die einzelnen Berechnungen und die Wall-Time (verstrichene Echtzeit) größer sein werden.
IO-gebundene Analyse
Andererseits ist bei der Durchführung von IO-gebundenen Operationen zu erwarten, dass ThreadPoolExecutor
leistungsfähiger zu sein als ProcessPoolExecutor
.
Die Threads von Python sind echte Betriebssystem-Threads. Sie können vom Betriebssystem in den Schlaf versetzt und wieder aufgeweckt werden, wenn ihre Informationen eintreffen.
Abschließende Überlegungen
Ich vermute, dass Multiprocessing unter Windows langsamer sein wird, da Windows kein Forking unterstützt, so dass jeder neue Prozess Zeit braucht, um zu starten.
Sie können mehrere Threads innerhalb mehrerer Prozesse verschachteln, aber es wird empfohlen, nicht mehrere Threads zu verwenden, um mehrere Prozesse abzuspalten.
Wenn man in Python mit einem schweren Verarbeitungsproblem konfrontiert wird, kann man trivialerweise mit zusätzlichen Prozessen skalieren - aber nicht so sehr mit Threading.