Versión: 2.6+, 3.x.
El módulo estándar multiprocessing
nos provee varios métodos para compartir información entre dos o más procesos, por lo que es necesario conocerlos a todos para decidir cuál se ajusta mejor a vuestras necesidades.
Punto de partida
Recordemos la forma en la que creamos un nuevo proceso.
from multiprocessing import Process def f(): print("Hola, mundo!") def main(): p = Process(target=f) p.start() if __name__ == "__main__": main()
Cabe aclarar que es particularmente importante la penúltima línea para el correcto funcionamiento del código.
Ahora bien, suponiendo que queremos indicarle a nuestro proceso hijo algunas personas a las que tiene que saludar, utilizamos el parámetro args
para pasar una lista. Por lo tanto, la función pasa a ser:
def f(names): for name in names: print("Hello, {0}!".format(name))
Y luego, la llamada:
names = ["Pedro", "Juan", "Jorge"] p = Process(target=f, args=(names,))
Como todos los objetos en Python son pasados «por referencia», convencionalmente podríamos modificar el valor de names desde f()
para agregar el nombre Luis.
def f(names): for name in names: print("Hello, {0}!".format(name)) names.append("Luis") names = ["Pedro", "Juan", "Jorge"] f(names) print(names)
El resultado en pantalla sería el siguiente.
Hello, Pedro!
Hello, Juan!
Hello, Jorge!
['Pedro', 'Juan', 'Jorge', 'Luis']
Sin embargo, recordemos que nuestra función f
en el ejemplo inicial se ejecuta en otro proceso, es decir, en un espacio de memoria diferente. Por lo tanto, desde el proceso hijo no es posible añadir un nombre a la lista y observar los cambios en el padre, pues lo que obtenemos no es más que una mera copia del objeto original que está alojada en una región diferente. Observamos este comportamiento en el siguiente código.
def f(names): for name in names: print("Hello, {0}!".format(name)) names.append("Luis") def main(): names = ["Pedro", "Juan", "Jorge"] p = Process(target=f, args=(names,)) p.start() p.join() # Esperar a que finalice la ejecución del proceso. print(names)
Imprime:
['Pedro', 'Juan', 'Jorge']
La lista original se mantiene intacta.
Objetos compartidos
Todos los objetos pasados vía args
al utilizar la clase Process
son serializados utilizando pickle
y enviados a través de un pipe al proceso hijo. De la misma forma pueden utilizarse diversas herramientas para realizar este procedimiento durante la ejecución del programa. Por ejemplo, multiprocessing
provee un sistema de colas similar al módulo queue
con la seguridad necesaria para ser utilizado por múltiples procesos.
from multiprocessing import Process, Queue def f(q): q.put([10.5, False, "Recursos Python"]) def main(): q = Queue() p = Process(target=f, args=(q,)) p.start() print(q.get()) if __name__ == "__main__": main()
Los objetos añadidos a la cola son transferidos entre procesos de la misma forma comentada anteriormente. Este sistema resulta ideal cuando se quiere compartir información entre más de dos procesos. Debido a las limitaciones de pickle
, puede que rara vez te encuentres con algún PickleError
por la incapacidad de serializar un objeto.
Internamente las colas utilizan una vía de comunicación llamada Pipe, similar a la arquitectura cliente-servidor del módulo socket
, para transferir los datos serializados. Por lo tanto, si la comunicación debe ser entre únicamente dos procesos, puede ser conveniente utilizar pipes.
from multiprocessing import Process, Pipe def f(pipe): pipe.send([10.5, False, "Recursos Python"]) def main(): server_pipe, client_pipe = Pipe() p = Process(target=f, args=(client_pipe,)) p.start() print(server_pipe.recv()) if __name__ == "__main__": main()
De esta manera, el proceso padre actúa como servidor y el hijo como cliente. La clase Pipe
retorna ambos «lados» de la conexión, y la parte del cliente es enviada al proceso hijo. Ambas partes pueden escribir (send
) y leer (recv
) datos, a menos que el parámetro opcional duplex
en Pipe
sea False
. En este caso, el servidor solo puede recibir y el cliente solo puede enviar.
Aún más internamente se utiliza un sistema de sincronización denominado Lock, justamente un «bloqueo» para evitar que varios procesos accedan a una misma información al mismo tiempo. El lock es adquirido por un proceso al realizar una operación de lectura o escritura y, al finalizar, es liberado. El objeto lock es compartido por todos los procesos y de esta manera un proceso no puede acceder a datos que estén siendo utilizados por otro. Raramente se presentan ocasiones para utilizar este tipo de sincronización directamente, pero la documentación oficial lo ha dotado de una buena utilidad: acceder a la consola de forma ordenada, evitando que los mensajes de un proceso se superpongan con los de otro.
# Ejemplo de la documentación oficial. from multiprocessing import Process, Lock def f(l, i): l.acquire() print 'hello world', i l.release() if __name__ == '__main__': lock = Lock() for num in range(10): Process(target=f, args=(lock, num)).start()
Memoria compartida
Por último, multiprocessing
también nos permite utilizar realmente memoria compartida. El proceso padre es dueño de un objeto al cual el resto puede acceder para leer o escribir. A diferencia de los métodos comentados anteriormente, los procesos hijos no obtienen una copia del objeto sino que acceden al original, por lo que solo pueden utilizarse los tipos que provee ctypes
.
El objeto puede ser uno de lo siguientes o bien un vector de cualquiera de ellos.
'c': ctypes.c_char 'u': ctypes.c_wchar 'b': ctypes.c_byte 'B': ctypes.c_ubyte 'h': ctypes.c_short 'H': ctypes.c_ushort 'i': ctypes.c_int 'I': ctypes.c_uint 'l': ctypes.c_long 'L': ctypes.c_ulong 'f': ctypes.c_float 'd': ctypes.c_double
La letra corresponde al identificador de cada tipo, pasado como argumento a Value
o Array
.
El siguiente ejemplo simula el funcionamiento de Process.join()
pero utilizando memoria compartida, un objeto del tipo ctypes.c_ubyte
que indica si el proceso hijo a finalizado con sus tareas.
from multiprocessing import Process, Value from time import sleep def f(finished): sleep(5) # Simular un procedimiento. finished.value = 1 def main(): finished = Value("B") finished.value = 0 p = Process(target=f, args=(finished,)) p.start() while not finished.value: pass if __name__ == "__main__": main()
Un vector de caracteres puede ser utilizado para representar una cadena al estilo C.
from multiprocessing import Process, Array from time import sleep def f(string): sleep(5) string.value = "Finalizado" def main(): # 10 indica el tamaño del vector. string = Array("c", 10) string.value = "Iniciado" p = Process(target=f, args=(string,)) p.start() while not string.value == "Finalizado": pass if __name__ == "__main__": main()
El módulo se encarga de utilizar un lock internamente para evitar la superposición al acceder al objeto compartido.
Curso online 👨💻
¡Ya lanzamos el curso oficial de Recursos Python en Udemy!
Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter
en 2024.
Consultoría 💡
Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.