multiprocessing.shared_memory — Mémoire partagée en accès direct depuis plusieurs processus

Code source : Lib/multiprocessing/shared_memory.py

Nouveau dans la version 3.8.


Ce module fournit une classe, SharedMemory, pour l'allocation et la gestion de mémoire partagée entre un ou plusieurs processus sur une machine à plusieurs cœurs ou à multiprocesseurs (architecture symmetric multiprocessor ou SMP). Pour faciliter la gestion du cycle de vie de la mémoire partagée, tout particulièrement entre plusieurs processus, le module multiprocessing.managers fournit aussi la classe SharedMemoryManager, sous-classe de BaseManager.

Dans ce module, il faut entendre « mémoire partagée » au sens de « blocs de mémoire partagée à la mode System V » (même si l'implémentation peut différer), et non au sens de « mémoire distribuée ». Ce type de mémoire partagée permet à plusieurs processus d'écrire dans une zone commune (ou « partagée ») de la mémoire vive. Normalement, les processus n'ont accès qu'à leur propre espace mémoire ; la mémoire partagée permet justement le partage de données entre des processus, ce qui leur évite d'avoir à s'envoyer ces données par message. Échanger des données par mémoire partagée peut amener des gains de performance substantiels par rapport aux échanges via le disque dur, des connecteurs ou d'autres canaux qui nécessitent de sérialiser et de désérialiser les données.

class multiprocessing.shared_memory.SharedMemory(name=None, create=False, size=0)

Crée un nouveau bloc de mémoire partagée ou enregistre un bloc déjà existant. Un nom unique doit être donné à chaque bloc de mémoire partagée ; ainsi, un processus peut créer un nouveau bloc de mémoire partagée avec un nom fixé et un autre processus peut enregistrer le même bloc, à partir de son nom.

Puisque qu'il permet de partager des données entre processus, un bloc de mémoire partagée peut survivre au processus qui l'a créé. Lorsqu'un processus n'a plus besoin d'un bloc — qui peut toujours être en cours d'utilisation par un autre — il doit appeler la méthode close(). Quand tous les processus ont fini d'utiliser ce bloc, il faut appeler la méthode unlink() pour le libérer.

name est le nom (une chaîne de caractères) unique de la mémoire partagée à allouer. Lors de la création d'un nouveau bloc mémoire, si None (valeur par défaut) est passé comme nom, un nouveau nom est généré.

create indique si un nouveau bloc doit être alloué (True) ou si on enregistre un bloc déjà existant (False).

size définit le nombre d'octets à allouer. Comme certaines plates-formes choisissent d'allouer les blocs mémoire en multiples de la taille de la page mémoire de la plate-forme, la taille réellement allouée peut être supérieure à la taille demandée. Lors de l'enregistrement d'un bloc déjà existant, le paramètre size est ignoré.

close()

Empêche les accès ultérieurs à la mémoire partagée depuis cette instance ; toutes les instances doivent appeler close() pour s'assurer que les ressources sont bien libérées. Notez qu'appeler close() ne libère pas la mémoire elle-même.

Initie la libération de la mémoire partagée sous-jacente. Pour être sûr que les ressources sont libérées correctement, unlink() doit être appelée une (et une seule) fois par tous les processus qui ont utilisé le bloc partagé. Après avoir initié la destruction d'un bloc mémoire, le bloc peut ne pas être détruit immédiatement ; ce comportement dépend de la plate-forme. Accéder aux données d'un bloc de mémoire partagée après l'appel à unlink() peut provoquer une erreur mémoire. Notez que le dernier processus à libérer le bloc mémoire de mémoire partagée peut appeler unlink() et close() dans n'importe quel ordre.

buf

Une memoryview du contenu du bloc de mémoire partagée.

name

Nom unique du bloc de mémoire partagée (lecture seule).

size

Taille en octets du bloc de mémoire partagée (lecture seule).

L'exemple qui suit montre un exemple d'utilisation bas niveau d'instances de SharedMemory :

>>> from multiprocessing import shared_memory
>>> shm_a = shared_memory.SharedMemory(create=True, size=10)
>>> type(shm_a.buf)
<class 'memoryview'>
>>> buffer = shm_a.buf
>>> len(buffer)
10
>>> buffer[:4] = bytearray([22, 33, 44, 55])  # Modify multiple at once
>>> buffer[4] = 100                           # Modify single byte at a time
>>> # Attach to an existing shared memory block
>>> shm_b = shared_memory.SharedMemory(shm_a.name)
>>> import array
>>> array.array('b', shm_b.buf[:5])  # Copy the data into a new array.array
array('b', [22, 33, 44, 55, 100])
>>> shm_b.buf[:5] = b'howdy'  # Modify via shm_b using bytes
>>> bytes(shm_a.buf[:5])      # Access via shm_a
b'howdy'
>>> shm_b.close()   # Close each SharedMemory instance
>>> shm_a.close()
>>> shm_a.unlink()  # Call unlink only once to release the shared memory

Le code qui suit est un exemple d'utilisation réel de la classe SharedMemory avec des tableaux NumPy qui accèdent au même numpy.ndarray depuis deux invites Python différentes :

>>> # In the first Python interactive shell
>>> import numpy as np
>>> a = np.array([1, 1, 2, 3, 5, 8])  # Start with an existing NumPy array
>>> from multiprocessing import shared_memory
>>> shm = shared_memory.SharedMemory(create=True, size=a.nbytes)
>>> # Now create a NumPy array backed by shared memory
>>> b = np.ndarray(a.shape, dtype=a.dtype, buffer=shm.buf)
>>> b[:] = a[:]  # Copy the original data into shared memory
>>> b
array([1, 1, 2, 3, 5, 8])
>>> type(b)
<class 'numpy.ndarray'>
>>> type(a)
<class 'numpy.ndarray'>
>>> shm.name  # We did not specify a name so one was chosen for us
'psm_21467_46075'

>>> # In either the same shell or a new Python shell on the same machine
>>> import numpy as np
>>> from multiprocessing import shared_memory
>>> # Attach to the existing shared memory block
>>> existing_shm = shared_memory.SharedMemory(name='psm_21467_46075')
>>> # Note that a.shape is (6,) and a.dtype is np.int64 in this example
>>> c = np.ndarray((6,), dtype=np.int64, buffer=existing_shm.buf)
>>> c
array([1, 1, 2, 3, 5, 8])
>>> c[-1] = 888
>>> c
array([  1,   1,   2,   3,   5, 888])

>>> # Back in the first Python interactive shell, b reflects this change
>>> b
array([  1,   1,   2,   3,   5, 888])

>>> # Clean up from within the second Python shell
>>> del c  # Unnecessary; merely emphasizing the array is no longer used
>>> existing_shm.close()

>>> # Clean up from within the first Python shell
>>> del b  # Unnecessary; merely emphasizing the array is no longer used
>>> shm.close()
>>> shm.unlink()  # Free and release the shared memory block at the very end
class multiprocessing.managers.SharedMemoryManager([address[, authkey]])

Une sous-classe de BaseManager pour gérer des blocs de mémoire partagée entre processus.

Un appel à start() depuis une instance SharedMemoryManager lance un nouveau processus dont le seul but est de gérer le cycle de vie des blocs mémoires qu'il a créés. La méthode shutdown() de l'instance déclenche la libération de tous les blocs mémoires gérés par ce processus. Elle appelle SharedMemory.unlink() sur tous les objets SharedMemory gérés par ce processus et l'arrête ensuite. Créer des instances de SharedMemory par l'intermédiaire d'un SharedMemoryManager évite d'avoir à gérer et à libérer manuellement les ressources mémoire partagées.

Cette classe fournit des méthodes pour créer et renvoyer des instances de SharedMemory et pour créer des objets compatibles liste (ShareableList) basés sur la mémoire partagée.

Référez-vous à multiprocessing.managers.BaseManager pour la description des arguments optionnels hérités address et authkey, et comment ceux-ci doivent être utilisés pour enregistrer un service de SharedMemoryManager depuis un autre processus.

SharedMemory(size)

Crée et renvoie un nouvel objet SharedMemory de taille size octets.

ShareableList(sequence)

Crée et renvoie un nouvel objet ShareableList, initialisé à partir des valeurs de la sequence en entrée.

L'exemple qui suit illustre les mécanismes de base de SharedMemoryManager :

>>> from multiprocessing.managers import SharedMemoryManager
>>> smm = SharedMemoryManager()
>>> smm.start()  # Start the process that manages the shared memory blocks
>>> sl = smm.ShareableList(range(4))
>>> sl
ShareableList([0, 1, 2, 3], name='psm_6572_7512')
>>> raw_shm = smm.SharedMemory(size=128)
>>> another_sl = smm.ShareableList('alpha')
>>> another_sl
ShareableList(['a', 'l', 'p', 'h', 'a'], name='psm_6572_12221')
>>> smm.shutdown()  # Calls unlink() on sl, raw_shm, and another_sl

L'exemple suivant montre comment utiliser un objet SharedMemoryManager avec l'instruction with pour être sûr que tous les blocs mémoire sont libérés quand ils ne sont plus nécessaires. C'est souvent plus pratique que l'exemple précédent :

>>> with SharedMemoryManager() as smm:
...     sl = smm.ShareableList(range(2000))
...     # Divide the work among two processes, storing partial results in sl
...     p1 = Process(target=do_work, args=(sl, 0, 1000))
...     p2 = Process(target=do_work, args=(sl, 1000, 2000))
...     p1.start()
...     p2.start()  # A multiprocessing.Pool might be more efficient
...     p1.join()
...     p2.join()   # Wait for all work to complete in both processes
...     total_result = sum(sl)  # Consolidate the partial results now in sl

Lors de l'utilisation d'un SharedMemoryManager dans une instruction with, les blocs de mémoire partagée créés par ce gestionnaire sont tous libérés quand les instructions à l'intérieur du bloc with ont été exécutées.

class multiprocessing.shared_memory.ShareableList(sequence=None, *, name=None)

Construit un objet muable compatible avec le type liste dont toutes les valeurs sont stockées dans un bloc de mémoire partagée. Ceci limite le type des valeurs pouvant être stockées aux types natifs int, float, bool, str (de moins de 10 Mo chacune), bytes (de moins de 10 Mo chacun) et None. Une autre différence majeure avec une list native réside dans le fait qu'il est impossible de changer la taille (c.-à-d. pas d'ajout en fin de liste, ni d'insertion etc.) et qu'il n'est pas possible de créer de nouvelles instances de ShareableList par découpage.

sequence sert à créer une nouvelle ShareableList avec des valeurs. Mettez-le à None pour enregistrer une ShareableList déjà existante, en renseignant son nom unique.

name est le nom unique de la mémoire partagée demandée, tel que décrit dans la définition de SharedMemory. Pour enregistrer une ShareableList déjà existante, renseignez le nom unique du bloc de mémoire partagée et laissez sequence à None.

count(value)

Renvoie le nombre d’occurrences de value.

index(value)

Renvoie l'indice de la première occurrence de value. Lève une ValueError si value n'est pas présent.

format

Attribut en lecture seule contenant le format d’agrégation struct utilisé par les valeurs déjà stockées.

shm

Instance de SharedMemory dans laquelle les valeurs sont stockées.

L'exemple qui suit illustre un cas d'usage de base d'une instance de ShareableList :

>>> from multiprocessing import shared_memory
>>> a = shared_memory.ShareableList(['howdy', b'HoWdY', -273.154, 100, None, True, 42])
>>> [ type(entry) for entry in a ]
[<class 'str'>, <class 'bytes'>, <class 'float'>, <class 'int'>, <class 'NoneType'>, <class 'bool'>, <class 'int'>]
>>> a[2]
-273.154
>>> a[2] = -78.5
>>> a[2]
-78.5
>>> a[2] = 'dry ice'  # Changing data types is supported as well
>>> a[2]
'dry ice'
>>> a[2] = 'larger than previously allocated storage space'
Traceback (most recent call last):
  ...
ValueError: exceeds available storage for existing str
>>> a[2]
'dry ice'
>>> len(a)
7
>>> a.index(42)
6
>>> a.count(b'howdy')
0
>>> a.count(b'HoWdY')
1
>>> a.shm.close()
>>> a.shm.unlink()
>>> del a  # Use of a ShareableList after call to unlink() is unsupported

L'exemple ci-dessous montre comment un, deux ou un grand nombre de processus peuvent accéder à une ShareableList commune à partir du nom du bloc mémoire partagé sous-jacent :

>>> b = shared_memory.ShareableList(range(5))         # In a first process
>>> c = shared_memory.ShareableList(name=b.shm.name)  # In a second process
>>> c
ShareableList([0, 1, 2, 3, 4], name='...')
>>> c[-1] = -999
>>> b[-1]
-999
>>> b.shm.close()
>>> c.shm.close()
>>> c.shm.unlink()

L'exemple ci-dessous montre comment une ShareableList (et le SharedMemory sous-jacent) peut être sérialisée et désérialisée. Gardez bien à l'esprit que c'est toujours le même objet, car l'objet désérialisé a le même nom unique et est tout simplement attaché à un objet déjà existant du même nom (si cet objet est toujours en vie).

>>> import pickle
>>> from multiprocessing import shared_memory
>>> sl = shared_memory.ShareableList(range(10))
>>> list(sl)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> deserialized_sl = pickle.loads(pickle.dumps(sl))
>>> list(deserialized_sl)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> sl[0] = -1
>>> deserialized_sl[1] = -2
>>> list(sl)
[-1, -2, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(deserialized_sl)
[-1, -2, 2, 3, 4, 5, 6, 7, 8, 9]
>>> sl.shm.close()
>>> sl.shm.unlink()