J'étais juste très confus par un code que j'ai écrit. J'ai été surpris de découvrir que:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(f, iterable))
et
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
results = list(map(lambda x: executor.submit(f, x), iterable))
produire des résultats différents. Le premier produit une liste de tout type retourné par f
, le second produit une liste d'objets concurrent.futures.Future
Qui doivent ensuite être évalués avec leur méthode result()
afin d'obtenir la valeur que f
a renvoyée.
Ma principale préoccupation est que cela signifie que executor.map
Ne peut pas tirer parti de concurrent.futures.as_completed
, Ce qui semble être un moyen extrêmement pratique d'évaluer les résultats de certains appels de longue durée vers une base de données que je ' m faire comme ils deviennent disponibles.
Je ne sais pas du tout comment les objets concurrent.futures.ThreadPoolExecutor
Fonctionnent - naïvement, je préférerais le (un peu plus verbeux):
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
result_futures = list(map(lambda x: executor.submit(f, x), iterable))
results = [f.result() for f in futures.as_completed(result_futures)]
sur le plus concis executor.map
afin de profiter d'un éventuel gain de performance. Ai-je tort de le faire?
Le problème est que vous transformez le résultat de ThreadPoolExecutor.map
à une liste. Si vous ne le faites pas et que vous effectuez plutôt une itération sur le générateur résultant directement, les résultats sont toujours produits dans le bon ordre mais la boucle continue avant que tous les résultats ne soient prêts. Vous pouvez tester cela avec cet exemple:
import time
import concurrent.futures
e = concurrent.futures.ThreadPoolExecutor(4)
s = range(10)
for i in e.map(time.sleep, s):
print(i)
Le maintien de l'ordre peut être dû au fait qu'il est parfois important d'obtenir des résultats dans le même ordre que celui que vous leur avez donné pour la carte. Et les résultats ne sont probablement pas encapsulés dans des objets futurs, car dans certaines situations, il peut prendre trop de temps pour faire une autre carte sur la liste pour obtenir tous les résultats si vous en avez besoin. Et après tout, dans la plupart des cas, il est très probable que la valeur suivante soit prête avant que la boucle ne traite la première valeur. Ceci est démontré dans cet exemple:
import concurrent.futures
executor = concurrent.futures.ThreadPoolExecutor() # Or ProcessPoolExecutor
data = some_huge_list()
results = executor.map(crunch_number, data)
finals = []
for value in results:
finals.append(do_some_stuff(value))
Dans cet exemple, il est probable que do_some_stuff
prend plus de temps que crunch_number
et si c'est vraiment le cas, ce n'est vraiment pas une grosse perte de performance pendant que vous gardez toujours la facilité d'utilisation de la carte.
De plus, puisque les threads de travail (/ processus) commencent le traitement au début de la liste et se dirigent vers la fin jusqu'à la liste que vous avez soumise, les résultats doivent être terminés dans l'ordre où ils sont déjà fournis par l'itérateur. Ce qui signifie dans la plupart des cas executor.map
est très bien, mais dans certains cas, par exemple, peu importe l'ordre dans lequel vous traitez les valeurs et la fonction que vous avez passée à map
prend des temps très différents pour s'exécuter, le future.as_completed
peut être plus rapide.
Voici un exemple de soumission vs carte. Ils acceptent tous les deux les travaux immédiatement (soumis | mappé - début). Ils prennent le même temps pour terminer, 11 secondes (temps du dernier résultat - début). Cependant, soumettre donne des résultats dès que n'importe quel thread dans ThreadPoolExecutor maxThreads = 2 se termine. la carte donne les résultats dans l'ordre où ils sont soumis.
import time
import concurrent.futures
def worker(i):
time.sleep(i)
return i,time.time()
e = concurrent.futures.ThreadPoolExecutor(2)
arrIn = range(1,7)[::-1]
print arrIn
f = []
print 'start submit',time.time()
for i in arrIn:
f.append(e.submit(worker,i))
print 'submitted',time.time()
for r in concurrent.futures.as_completed(f):
print r.result(),time.time()
print
f = []
print 'start map',time.time()
f = e.map(worker,arrIn)
print 'mapped',time.time()
for r in f:
print r,time.time()
Production:
[6, 5, 4, 3, 2, 1]
start submit 1543473934.47
submitted 1543473934.47
(5, 1543473939.473743) 1543473939.47
(6, 1543473940.471591) 1543473940.47
(3, 1543473943.473639) 1543473943.47
(4, 1543473943.474192) 1543473943.47
(1, 1543473944.474617) 1543473944.47
(2, 1543473945.477609) 1543473945.48
start map 1543473945.48
mapped 1543473945.48
(6, 1543473951.483908) 1543473951.48
(5, 1543473950.484109) 1543473951.48
(4, 1543473954.48858) 1543473954.49
(3, 1543473954.488384) 1543473954.49
(2, 1543473956.493789) 1543473956.49
(1, 1543473955.493888) 1543473956.49
En plus de l'explication dans les réponses ici, il peut être utile d'aller directement à la source. Il réaffirme la déclaration d'une autre réponse ici:
.map()
donne les résultats dans l'ordre où ils sont soumis, tandis queFuture
avec concurrent.futures.as_completed()
ne garantira pas cet ordre, car c'est la nature de as_completed()
.map()
est définie dans la classe de base, concurrent.futures._base.Executor
:
class Executor(object):
def submit(self, fn, *args, **kwargs):
raise NotImplementedError()
def map(self, fn, *iterables, timeout=None, chunksize=1):
if timeout is not None:
end_time = timeout + time.monotonic()
fs = [self.submit(fn, *args) for args in Zip(*iterables)] # <!!!!!!!!
def result_iterator():
try:
# reverse to keep finishing order
fs.reverse() # <!!!!!!!!
while fs:
# Careful not to keep a reference to the popped future
if timeout is None:
yield fs.pop().result() # <!!!!!!!!
else:
yield fs.pop().result(end_time - time.monotonic())
finally:
for future in fs:
future.cancel()
return result_iterator()
Comme vous le mentionnez, il y a aussi .submit()
, qui reste à définir dans les classes enfants, à savoir ProcessPoolExecutor
et ThreadPoolExecutor
, et retourne un _base.Future
Instance que vous devez appeler .result()
pour réellement faire quoi que ce soit.
Les lignes importantes de .map()
se résument à:
fs = [self.submit(fn, *args) for args in Zip(*iterables)]
fs.reverse()
while fs:
yield fs.pop().result()
.reverse()
plus .pop()
est un moyen d'obtenir le premier résultat soumis (à partir de iterables
) à produire en premier, le deuxième résultat soumis à produire en second, etc. Les éléments de l'itérateur résultant ne sont pas Future
s; ce sont les résultats réels eux-mêmes.