93 votes

La mémoire partagée dans le multitraitement

J'ai trois grandes listes. La première contient des tableaux de bits (module bitarray 0.8.0) et les deux autres des tableaux d'entiers.

l1=[bitarray 1, bitarray 2, ... ,bitarray n]
l2=[array 1, array 2, ... , array n]
l3=[array 1, array 2, ... , array n]

Ces structures de données occupent une grande partie de la RAM (~16GB au total).

Si je lance 12 sous-processus en utilisant :

multiprocessing.Process(target=someFunction, args=(l1,l2,l3))

Cela signifie-t-il que l1, l2 et l3 seront copiés pour chaque sous-processus ou que les sous-processus partageront ces listes ? Ou, pour être plus direct, est-ce que j'utiliserai 16 Go ou 192 Go de RAM ?

someFunction lira certaines valeurs de ces listes et effectuera ensuite certains calculs sur la base des valeurs lues. Les résultats seront renvoyés au processus parent. Les listes l1, l2 et l3 ne seront pas modifiées par someFunction.

Par conséquent, je suppose que les sous-processus n'ont pas besoin et ne copient pas ces énormes listes, mais les partagent simplement avec le parent. Cela signifie que le programme prendrait 16 Go de RAM (quel que soit le nombre de sous-processus que je lance) en raison de l'approche "copy-on-write" sous Linux ? Ai-je raison ou ai-je manqué quelque chose qui ferait que les listes seraient copiées ?

EDITAR : Je suis toujours confus, après avoir lu un peu plus sur le sujet. D'un côté, Linux utilise le copy-on-write, ce qui devrait signifier qu'aucune donnée n'est copiée. D'autre part, l'accès à l'objet changera son ref-count (je ne sais toujours pas pourquoi et ce que cela signifie). Malgré cela, l'objet entier sera-t-il copié ?

Par exemple, si je définis une certaineFonction comme suit :

def someFunction(list1, list2, list3):
    i=random.randint(0,99999)
    print list1[i], list2[i], list3[i]

L'utilisation de cette fonction signifie-t-elle que l1, l2 et l3 seront entièrement copiés pour chaque sous-processus ?

Y a-t-il un moyen de vérifier cela ?

EDIT2 Après avoir lu un peu plus et surveillé l'utilisation totale de la mémoire du système pendant l'exécution des sous-processus, il semble que des objets entiers soient effectivement copiés pour chaque sous-processus. Et cela semble être dû au comptage des références.

Le comptage des références pour l1, l2 et l3 est en fait inutile dans mon programme. En effet, les listes l1, l2 et l3 seront conservées en mémoire (sans modification) jusqu'à la sortie du processus parent. Il n'y a pas besoin de libérer la mémoire utilisée par ces listes jusque là. En fait, je sais avec certitude que le nombre de références restera supérieur à 0 (pour ces listes et chaque objet de ces listes) jusqu'à ce que le programme se termine.

La question est donc la suivante : comment puis-je m'assurer que les objets ne seront pas copiés dans chaque sous-processus ? Puis-je désactiver le comptage des références pour ces listes et chaque objet de ces listes ?

EDIT3 Une remarque supplémentaire. Les sous-processus n'ont pas besoin de modifier l1 , l2 y l3 ou tout autre objet de ces listes. Les sous-processus doivent seulement pouvoir faire référence à certains de ces objets sans que la mémoire soit copiée pour chaque sous-processus.

73voto

Rboreal_Frippery Points 1004

Comme il s'agit toujours d'un résultat très élevé sur Google et que personne d'autre ne l'a encore mentionné, j'ai pensé que je mentionnerais la nouvelle possibilité de la "vraie" mémoire partagée qui a été introduite dans la version 3.8.0 de Python : https://docs.python.org/3/library/multiprocessing.shared_memory.html

J'ai inclus ici un petit exemple artificiel (testé sous linux) où des tableaux numpy sont utilisés, ce qui est probablement un cas d'utilisation très courant :

# one dimension of the 2d array which is shared
dim = 5000

import numpy as np
from multiprocessing import shared_memory, Process, Lock
from multiprocessing import cpu_count, current_process
import time

lock = Lock()

def add_one(shr_name):

    existing_shm = shared_memory.SharedMemory(name=shr_name)
    np_array = np.ndarray((dim, dim,), dtype=np.int64, buffer=existing_shm.buf)
    lock.acquire()
    np_array[:] = np_array[0] + 1
    lock.release()
    time.sleep(10) # pause, to see the memory usage in top
    print('added one')
    existing_shm.close()

def create_shared_block():

    a = np.ones(shape=(dim, dim), dtype=np.int64)  # Start with an existing NumPy array

    shm = shared_memory.SharedMemory(create=True, size=a.nbytes)
    # # Now create a NumPy array backed by shared memory
    np_array = np.ndarray(a.shape, dtype=np.int64, buffer=shm.buf)
    np_array[:] = a[:]  # Copy the original data into shared memory
    return shm, np_array

if current_process().name == "MainProcess":
    print("creating shared block")
    shr, np_array = create_shared_block()

    processes = []
    for i in range(cpu_count()):
        _process = Process(target=add_one, args=(shr.name,))
        processes.append(_process)
        _process.start()

    for _process in processes:
        _process.join()

    print("Final array")
    print(np_array[:10])
    print(np_array[10:])

    shr.close()
    shr.unlink()

Notez qu'en raison de l'utilisation d'ints 64 bits, ce code peut nécessiter environ 1 Go de mémoire vive pour s'exécuter, alors assurez-vous que vous ne risquez pas de geler votre système en l'utilisant. ^_^

71voto

Roberto Liffredo Points 15265

D'une manière générale, il existe deux façons de partager les mêmes données :

  • Multithreading
  • Mémoire partagée

Le multithreading de Python n'est pas adapté aux tâches liées au CPU (à cause de la GIL), donc la solution habituelle dans ce cas est de passer sur multiprocessing . Cependant, avec cette solution, vous devez partager explicitement les données, en utilisant la fonction multiprocessing.Value y multiprocessing.Array .

Notez que le partage de données entre processus n'est généralement pas le meilleur choix, en raison de tous les problèmes de synchronisation ; une approche impliquant des acteurs échangeant des messages est généralement considérée comme un meilleur choix. Voir aussi Documentation Python :

Comme mentionné ci-dessus, lorsqu'on fait de la programmation concurrente, il est généralement d'éviter autant que possible d'utiliser un état partagé. Ceci est particulièrement vrai lorsque l'on utilise plusieurs processus.

Cependant, si vous avez vraiment besoin d'utiliser des données partagées, alors le multiprocessing offre plusieurs façons de le faire.

Dans votre cas, vous devez envelopper l1 , l2 y l3 d'une manière compréhensible par multiprocessing (par exemple, en utilisant un multiprocessing.Array ), puis de les passer comme paramètres.
Notez également que, comme vous avez dit que vous n'avez pas besoin d'accès en écriture, alors vous devriez passer lock=False lors de la création des objets, sinon tous les accès seront encore sérialisés.

12voto

thuzhf Points 503

Pour les personnes intéressées par l'utilisation de l'algorithme de Python3.8 mémoire partagée il dispose toujours d'un module bogue qui n'a pas été corrigé et qui affecte Python3.8/3.9/3.10 à ce jour (2021-01-15). Le bogue affecte les systèmes posix et concerne le traqueur de ressources qui détruit des segments de mémoire partagée alors que d'autres processus devraient encore y avoir un accès valide. Faites donc attention si vous l'utilisez dans votre code.

11voto

Turnaev Evgeny Points 474

Si vous voulez utiliser la fonction copy-on-write et que vos données sont statiques (inchangées dans les processus enfants), vous devez faire en sorte que python ne s'occupe pas des blocs mémoire où se trouvent vos données. Vous pouvez facilement faire cela en utilisant des structures C ou C++ (stl par exemple) comme conteneurs et fournir vos propres wrappers python qui utiliseront des pointeurs vers la mémoire des données (ou éventuellement copier la mémoire des données) lorsque l'objet de niveau python sera créé, s'il y en a un. Tout ceci peut être fait très facilement avec une simplicité et une syntaxe proche de celle de python avec cython .

\# pseudo cython
cdef class FooContainer:
   cdef char \* data
   def \_\_cinit\_\_(self, char \* foo\_value):
       self.data = malloc(1024, sizeof(char))
       memcpy(self.data, foo\_value, min(1024, len(foo\_value)))

   def get(self):
       return self.data

    \# python part
from foo import FooContainer

f = FooContainer("hello world")
pid = fork()
if not pid:
   f.get() # this call will read same memory page to where
           # parent process wrote 1024 chars of self.data
           # and cython will automatically create a new python string
           # object from it and return to caller

Le pseudo-code ci-dessus est mal écrit. Ne l'utilisez pas. A la place de self.data il faut mettre un conteneur C ou C++ dans votre cas.

3voto

Vous pouvez utiliser memcached ou redis et les définir comme une paire clé-valeur. {'l1'...

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X