web-dev-qa-db-fra.com

Comment combiner Pool.map avec Array (mémoire partagée) en Python multiprocessing?

J'ai un très grand tableau (en lecture seule) de données que je souhaite être traité par plusieurs processus en parallèle.

J'aime la fonction Pool.map et je voudrais l'utiliser pour calculer des fonctions sur ces données en parallèle.

J'ai vu que l'on peut utiliser la classe Value ou Array pour utiliser des données de mémoire partagée entre les processus. Mais lorsque j'essaie d'utiliser cela, j'obtiens un RuntimeError: 'Les objets SynchronizedString ne doivent être partagés entre les processus via l'héritage que lorsque vous utilisez la fonction Pool.map:

Voici un exemple simplifié de ce que j'essaie de faire:

from sys import stdin
from multiprocessing import Pool, Array

def count_it( arr, key ):
  count = 0
  for c in arr:
    if c == key:
      count += 1
  return count

if __== '__main__':
  testData = "abcabcs bsdfsdf gdfg dffdgdfg sdfsdfsd sdfdsfsdf"
  # want to share it using shared memory
  toShare = Array('c', testData)

  # this works
  print count_it( toShare, "a" )

  pool = Pool()

  # RuntimeError here
  print pool.map( count_it, [(toShare,key) for key in ["a", "b", "s", "d"]] )

Quelqu'un peut-il me dire ce que je fais mal ici?

Donc, ce que je voudrais faire, c'est transmettre des informations sur un tableau alloué de mémoire partagée nouvellement créé aux processus après qu'ils ont été créés dans le pool de processus.

51
Jeroen Dirks

J'essaye encore car je viens de voir la prime;)

Fondamentalement, je pense que le message d'erreur signifie ce qu'il a dit - les tableaux de mémoire partagée multiprocesseurs ne peuvent pas être passés comme arguments (par décapage). Il n'est pas logique de sérialiser les données - le fait est que les données sont de la mémoire partagée. Vous devez donc rendre le tableau partagé global. Je pense qu'il est plus judicieux de le mettre comme attribut d'un module, comme dans ma première réponse, mais le laisser simplement comme variable globale dans votre exemple fonctionne également bien. Prenant en compte votre point de ne pas vouloir mettre les données avant le fork, voici un exemple modifié. Si vous vouliez avoir plus d'un tableau partagé possible (et c'est pourquoi vous vouliez passer à Share en tant qu'argument), vous pourriez également faire une liste globale des tableaux partagés, et simplement passer l'index à count_it (qui deviendrait for c in toShare[i]:).

from sys import stdin
from multiprocessing import Pool, Array, Process

def count_it( key ):
  count = 0
  for c in toShare:
    if c == key:
      count += 1
  return count

if __== '__main__':
  # allocate shared array - want lock=False in this case since we 
  # aren't writing to it and want to allow multiple processes to access
  # at the same time - I think with lock=True there would be little or 
  # no speedup
  maxLength = 50
  toShare = Array('c', maxLength, lock=False)

  # fork
  pool = Pool()

  # can set data after fork
  testData = "abcabcs bsdfsdf gdfg dffdgdfg sdfsdfsd sdfdsfsdf"
  if len(testData) > maxLength:
      raise ValueError, "Shared array too small to hold data"
  toShare[:len(testData)] = testData

  print pool.map( count_it, ["a", "b", "s", "d"] )

[EDIT: Ce qui précède ne fonctionne pas sur Windows car vous n'utilisez pas fork. Cependant, ce qui suit fonctionne sous Windows, toujours en utilisant Pool, donc je pense que c'est le plus proche de ce que vous voulez:

from sys import stdin
from multiprocessing import Pool, Array, Process
import mymodule

def count_it( key ):
  count = 0
  for c in mymodule.toShare:
    if c == key:
      count += 1
  return count

def initProcess(share):
  mymodule.toShare = share

if __== '__main__':
  # allocate shared array - want lock=False in this case since we 
  # aren't writing to it and want to allow multiple processes to access
  # at the same time - I think with lock=True there would be little or 
  # no speedup
  maxLength = 50
  toShare = Array('c', maxLength, lock=False)

  # fork
  pool = Pool(initializer=initProcess,initargs=(toShare,))

  # can set data after fork
  testData = "abcabcs bsdfsdf gdfg dffdgdfg sdfsdfsd sdfdsfsdf"
  if len(testData) > maxLength:
      raise ValueError, "Shared array too small to hold data"
  toShare[:len(testData)] = testData

  print pool.map( count_it, ["a", "b", "s", "d"] )

Je ne sais pas pourquoi la carte ne décapera pas le tableau, mais Process and Pool le fera - je pense qu'il a peut-être été transféré au moment de l'initialisation du sous-processus sur Windows. Notez que les données sont toujours définies après le fork.

41
robince

Le problème que je vois est que Pool ne prend pas en charge le décapage des données partagées via sa liste d'arguments. C'est ce que le message d'erreur signifie par "les objets ne doivent être partagés qu'entre les processus par héritage". Les données partagées doivent être héritées, c'est-à-dire globales si vous souhaitez les partager à l'aide de la classe Pool.

Si vous devez les transmettre explicitement, vous devrez peut-être utiliser le multiprocessing.Process. Voici votre exemple retravaillé:

from multiprocessing import Process, Array, Queue

def count_it( q, arr, key ):
  count = 0
  for c in arr:
    if c == key:
      count += 1
  q.put((key, count))

if __== '__main__':
  testData = "abcabcs bsdfsdf gdfg dffdgdfg sdfsdfsd sdfdsfsdf"
  # want to share it using shared memory
  toShare = Array('c', testData)

  q = Queue()
  keys = ['a', 'b', 's', 'd']
  workers = [Process(target=count_it, args = (q, toShare, key))
    for key in keys]

  for p in workers:
    p.start()
  for p in workers:
    p.join()
  while not q.empty():
    print q.get(),

Sortie: ('s', 9) ('a', 2) ('b', 3) ('d', 12)

L'ordre des éléments de la file d'attente peut varier.

Pour rendre cela plus générique et similaire à Pool, vous pouvez créer un nombre N fixe de processus, diviser la liste de clés en N morceaux, puis utiliser une fonction wrapper comme cible de processus, qui appellera count_it pour chaque clé de la liste c'est passé, comme:

def wrapper( q, arr, keys ):
  for k in keys:
    count_it(q, arr, k)
5
jwilson

Si les données sont en lecture seule, il suffit d'en faire une variable dans un module avant le fork de Pool. Ensuite, tous les processus enfants devraient pouvoir y accéder, et il ne sera pas copié à condition de ne pas y écrire.

import myglobals # anything (empty .py file)
myglobals.data = []

def count_it( key ):
    count = 0
    for c in myglobals.data:
        if c == key:
            count += 1
    return count

if __== '__main__':
myglobals.data = "abcabcs bsdfsdf gdfg dffdgdfg sdfsdfsd sdfdsfsdf"

pool = Pool()
print pool.map( count_it, ["a", "b", "s", "d"] )

Si vous voulez essayer d'utiliser Array, vous pouvez essayer avec le lock=False argument mot-clé (c'est vrai par défaut).

2
robince