web-dev-qa-db-fra.com

Comment faire de la programmation parallèle en Python

Pour C++, nous pouvons utiliser OpenMP pour faire de la programmation parallèle. Cependant, OpenMP ne fonctionnera pas pour Python. Que dois-je faire si je veux mettre en parallèle certaines parties de mon programme python?

La structure du code peut être considérée comme:

 solve1(A)
 solve2(B)

solve1 et solve2 sont deux fonctions indépendantes. Comment exécuter ce type de code en parallèle plutôt qu'en séquence afin de réduire le temps d'exécution? Merci beaucoup d'avance ... Le code est:

def solve(Q, G, n):
    i = 0
    tol = 10 ** -4

    while i < 1000:
        inneropt, partition, x = setinner(Q, G, n)
        outeropt = setouter(Q, G, n)

        if (outeropt - inneropt) / (1 + abs(outeropt) + abs(inneropt)) < tol:
            break

        node1 = partition[0]
        node2 = partition[1]

        G = updateGraph(G, node1, node2)

        if i == 999:
            print "Maximum iteration reaches"
    print inneropt

Où setinner et setouter sont deux fonctions indépendantes. C'est là que je veux mettre en parallèle ...

109
ilovecp3

Vous pouvez utiliser le module multitraitement . Dans ce cas, je pourrais utiliser un pool de traitement:

from multiprocessing import Pool
pool = Pool()
result1 = pool.apply_async(solve1, [A])    # evaluate "solve1(A)" asynchronously
result2 = pool.apply_async(solve2, [B])    # evaluate "solve2(B)" asynchronously
answer1 = result1.get(timeout=10)
answer2 = result2.get(timeout=10)

Cela engendrera des processus pouvant effectuer un travail générique à votre place. Puisque nous n'avons pas passé processes, il créera un processus pour chaque cœur de processeur de votre ordinateur. Chaque cœur de processeur peut exécuter un processus simultanément.

Si vous souhaitez mapper une liste à une seule fonction, procédez comme suit:

args = [A, B]
results = pool.map(solve1, args)

N'utilisez pas de threads, car GIL verrouille les opérations sur les objets python. 

127
Matt Williamson

Cela peut être fait très élégamment avec Ray .

Pour paralléliser votre exemple, vous devez définir vos fonctions avec le décorateur @ray.remote, puis les appeler avec .remote.

import ray

ray.init()

# Define the functions.

@ray.remote
def solve1(a):
    return 1

@ray.remote
def solve2(b):
    return 2

# Start two tasks in the background.
x_id = solve1.remote(0)
y_id = solve2.remote(1)

# Block until the tasks are done and get the results.
x, y = ray.get([x_id, y_id])

Cela présente de nombreux avantages par rapport au module multitraitement .

  1. Le même code sera exécuté sur une machine multicœur ainsi que sur un cluster de machines.
  2. Les processus partagent efficacement les données via la mémoire partagée et la sérialisation sans copie .
  3. Les messages d'erreur se propagent bien.
  4. Ces appels de fonction peuvent être composés ensemble, par exemple,

    @ray.remote
    def f(x):
        return x + 1
    
    x_id = f.remote(1)
    y_id = f.remote(x_id)
    z_id = f.remote(y_id)
    ray.get(z_id)  # returns 4
    
  5. En plus d'appeler des fonctions à distance, les classes peuvent être instanciées à distance en tant que acteurs .

Notez que Ray est un framework que j'ai aidé à développer.

8
Robert Nishihara

CPython utilise le Global Interpreter Lock, ce qui rend la programmation parallèle un peu plus intéressante que C++.

Ce sujet contient plusieurs exemples et descriptions utiles du défi:

Solution de contournement du verrou d'interpréteur global Python (GIL) sur les systèmes multicœurs utilisant des tâches sur Linux?

3
bauman.space

La solution, comme d'autres l'ont dit, consiste à utiliser plusieurs processus. Le cadre le plus approprié dépend toutefois de nombreux facteurs. En plus de ceux déjà mentionnés, il y a aussi charm4py et mpi4py (je suis le développeur de charm4py).

Il existe un moyen plus efficace d'implémenter l'exemple ci-dessus que d'utiliser l'abstraction du pool de travail. La boucle principale envoie les mêmes paramètres (y compris le graphe complet G) à plusieurs reprises aux travailleurs dans chacune des 1000 itérations. Puisqu'au moins un travailleur réside sur un processus différent, cela implique la copie et l'envoi des arguments à l'autre processus. Cela pourrait être très coûteux en fonction de la taille des objets. Au lieu de cela, il est logique que les travailleurs stockent l'état et envoient simplement les informations mises à jour.

Par exemple, dans charm4py cela peut être fait comme ceci:

class Worker(Chare):

    def __init__(self, Q, G, n):
        self.G = G
        ...

    def setinner(self, node1, node2):
        self.updateGraph(node1, node2)
        ...


def solve(Q, G, n):
    # create 2 workers, each on a different process, passing the initial state
    worker_a = Chare(Worker, onPE=0, args=[Q, G, n])
    worker_b = Chare(Worker, onPE=1, args=[Q, G, n])
    while i < 1000:
        result_a = worker_a.setinner(node1, node2, ret=True)  # execute setinner on worker A
        result_b = worker_b.setouter(node1, node2, ret=True)  # execute setouter on worker B

        inneropt, partition, x = result_a.get()  # wait for result from worker A
        outeropt = result_b.get()  # wait for result from worker B
        ...

Notez que pour cet exemple, nous n'avons besoin que d'un seul ouvrier. La boucle principale peut exécuter l'une des fonctions et faire en sorte que l'opérateur exécute l'autre. Mais mon code aide à illustrer un certain nombre de choses:

  1. Le travailleur A est exécuté dans le processus 0 (identique à la boucle principale). Alors que result_a.get() est bloqué en attente du résultat, le travailleur A effectue le calcul dans le même processus.
  2. Les arguments sont automatiquement passés par référence à l'agent A, puisqu'il se trouve dans le même processus (Aucune copie n'est impliquée).
0
Juan Galvez