web-dev-qa-db-fra.com

Comment multi-thread d'une opération dans une boucle dans Python

Disons que j'ai une très grande liste et que j'effectue une opération comme celle-ci:

for item in items:
    try:
        api.my_operation(item)
    except:
        print 'error with item'

Mon problème est double:

  • Il y a beaucoup d'articles
  • api.my_operation prend une éternité pour revenir

J'aimerais utiliser le multi-threading pour faire tourner un tas d'api.my_operations à la fois afin que je puisse traiter peut-être 5 ou 10 ou même 100 éléments à la fois.

Si my_operation () renvoie une exception (parce que j'ai peut-être déjà traité cet élément) - c'est OK. Cela ne cassera rien. La boucle peut passer à l'élément suivant.

Note: c'est pour Python 2.7.3

45
doremi

Tout d'abord, en Python, si votre code est lié au processeur, le multithreading n'aidera pas, car un seul thread peut contenir le verrouillage de l'interpréteur global, et donc exécuter Python, à la fois. Donc , vous devez utiliser des processus, pas des threads.

Ce n'est pas vrai si votre opération "prend une éternité à revenir" car elle est liée aux E/S, c'est-à-dire en attente sur le réseau ou des copies de disque ou similaires. J'y reviendrai plus tard.


Ensuite, la façon de traiter 5 ou 10 ou 100 éléments à la fois consiste à créer un pool de 5 ou 10 ou 100 travailleurs et de placer les éléments dans une file d'attente que le service gère. Heureusement, le stdlib multiprocessing et concurrent.futures les bibliothèques résument pour vous la plupart des détails.

Le premier est plus puissant et plus flexible pour la programmation traditionnelle; ce dernier est plus simple si vous avez besoin de composer une attente future; pour les cas triviaux, peu importe ce que vous choisissez. (Dans ce cas, l'implémentation la plus évidente avec chacun prend 3 lignes avec futures, 4 lignes avec multiprocessing.)

Si vous utilisez 2.6-2.7 ou 3.0-3.1, futures n'est pas intégré, mais vous pouvez l'installer à partir de PyPI (pip install futures).


Enfin, il est généralement beaucoup plus simple de paralléliser les choses si vous pouvez transformer l'itération de la boucle entière en un appel de fonction (quelque chose que vous pourriez, par exemple, passer à map), alors faisons-le d'abord:

def try_my_operation(item):
    try:
        api.my_operation(item)
    except:
        print('error with item')

Mettre tous ensemble:

executor = concurrent.futures.ProcessPoolExecutor(10)
futures = [executor.submit(try_my_operation, item) for item in items]
concurrent.futures.wait(futures)

Si vous avez beaucoup de travaux relativement petits, les frais généraux du multitraitement peuvent inonder les gains. La façon de résoudre ce problème consiste à regrouper le travail en tâches plus importantes. Par exemple (en utilisant grouper à partir des itertools recettes , que vous pouvez copier et coller dans votre code, ou obtenir à partir du more-itertools projet sur PyPI):

def try_multiple_operations(items):
    for item in items:
        try:
            api.my_operation(item)
        except:
            print('error with item')

executor = concurrent.futures.ProcessPoolExecutor(10)
futures = [executor.submit(try_multiple_operations, group) 
           for group in grouper(5, items)]
concurrent.futures.wait(futures)

Enfin, que se passe-t-il si votre code est IO lié? Alors les threads sont tout aussi bons que les processus, et avec moins de surcharge (et moins de limitations, mais ces limitations ne vous affectent généralement pas dans des cas comme celui-ci) Parfois, ce "moins de frais généraux" suffit pour signifier que vous n'avez pas besoin de regrouper avec des threads, mais vous le faites avec des processus, ce qui est une belle victoire.

Alors, comment utilisez-vous les threads au lieu des processus? Changez simplement ProcessPoolExecutor en ThreadPoolExecutor.

Si vous ne savez pas si votre code est lié au processeur ou lié aux E/S, essayez-le dans les deux sens.


Puis-je faire cela pour plusieurs fonctions dans mon script python? Par exemple, si j'avais une autre boucle for ailleurs dans le code que je voulais paralléliser. Est-il possible de faire deux fonctions multi-threadées dans le même script?

Oui. En fait, il existe deux façons différentes de le faire.

Tout d'abord, vous pouvez partager le même exécuteur (thread ou processus) et l'utiliser sans problème à partir de plusieurs endroits. L'intérêt des tâches et des futurs est qu'ils sont autonomes; vous ne vous souciez pas où ils courent, juste que vous les mettez en file d'attente et obtenez finalement la réponse.

Alternativement, vous pouvez avoir deux exécuteurs dans le même programme sans problème. Cela a un coût de performance: si vous utilisez les deux exécuteurs en même temps, vous finirez par essayer d'exécuter (par exemple) 16 threads occupés sur 8 cœurs, ce qui signifie qu'il y aura un changement de contexte. Mais parfois, cela vaut la peine car, disons, les deux exécuteurs sont rarement occupés en même temps, et cela rend votre code beaucoup plus simple. Ou peut-être qu'un exécuteur exécute de très grandes tâches qui peuvent prendre un certain temps et l'autre exécute de très petites tâches qui doivent être terminées le plus rapidement possible, car la réactivité est plus importante que le débit pour une partie de votre programme.

Si vous ne savez pas ce qui convient à votre programme, c'est généralement le premier.

83
abarnert

Edit 2018-02-06 : révision basée sur ce commentaire

Edit : oublié de mentionner que cela fonctionne sur Python 2.7.x

Il y a multiprocesing.pool, et l'exemple suivant illustre comment utiliser l'un d'eux:

from multiprocessing.pool import ThreadPool as Pool
# from multiprocessing import Pool

pool_size = 5  # your "parallelness"

# define worker function before a Pool is instantiated
def worker(item):
    try:
        api.my_operation(item)
    except:
        print('error with item')

pool = Pool(pool_size)

for item in items:
    pool.apply_async(worker, (item,))

pool.close()
pool.join()

Maintenant, si vous identifiez en effet que votre processus est lié au processeur comme @abarnert l'a mentionné, changez ThreadPool en implémentation du pool de processus (commenté sous importation ThreadPool). Vous pouvez trouver plus de détails ici: http://docs.python.org/2/library/multiprocessing.html#using-a-pool-of-workers

21
woozyking

Vous pouvez diviser le traitement en un nombre spécifié de threads en utilisant une approche comme celle-ci:

import threading                                                                

def process(items, start, end):                                                 
    for item in items[start:end]:                                               
        try:                                                                    
            api.my_operation(item)                                              
        except Exception:                                                       
            print('error with item')                                            


def split_processing(items, num_splits=4):                                      
    split_size = len(items) // num_splits                                       
    threads = []                                                                
    for i in range(num_splits):                                                 
        # determine the indices of the list this thread will handle             
        start = i * split_size                                                  
        # special case on the last chunk to account for uneven splits           
        end = None if i+1 == num_splits else (i+1) * split_size                 
        # create the thread                                                     
        threads.append(                                                         
            threading.Thread(target=process, args=(items, start, end)))         
        threads[-1].start() # start the thread we just created                  

    # wait for all threads to finish                                            
    for t in threads:                                                           
        t.join()                                                                



split_processing(items)
10
Ryan Haining
import numpy as np
import threading


def threaded_process(items_chunk):
    """ Your main process which runs in thread for each chunk"""
    for item in items_chunk:                                               
        try:                                                                    
            api.my_operation(item)                                              
        except Exception:                                                       
            print('error with item')  

n_threads = 20
# Splitting the items into chunks equal to number of threads
array_chunk = np.array_split(input_image_list, n_threads)
thread_list = []
for thr in range(n_threads):
    thread = threading.Thread(target=threaded_process, args=(array_chunk[thr]),)
    thread_list.append(thread)
    thread_list[thr].start()

for thread in thread_list:
    thread.join()
0
Vinoj John Hosan