1️⃣
Procesos en Python
Introducción a la clase [Link]
[Link] está bien para paralelismo de datos, pero necesitamos más flexibilidad.
Hasta ahora, hemos usado [Link] para aplicar una misma función a una lista de datos (paralelismo de datos). Esto es
muy cómodo, pero rígido: todos los procesos hacen lo mismo y el programa principal se queda bloqueado esperando a
que todos terminen para recibir una lista de resultados.
Clase [Link]
La clase Process es la herramienta de bajo nivel. Te permite crear procesos individuales donde cada uno puede hacer una
tarea totalmente distinta a los demás. No estás limitado a "aplicar una función a una lista"; puedes tener un proceso
vigilando un sensor, otro escribiendo en un archivo y otro calculando una suma, todo a la vez.
[Link]
class [Link](group=None, target=None, name=None, args=(), kwargs={}, ..., daem
on=None)
Los objetos Process representan una actividad que se ejecuta en un proceso separado (cada uno con su propia memoria, no
comparten variables directamente, como lo hacen los hilos de Thread ). La clase Process tiene equivalentes para todos los
métodos de [Link] .
El constructor siempre debe llamarse con argumentos keyword:
group siempre debe ser None (solo existe por compatibilidad con [Link] ).
es el objeto (función) que será llamado por el método run() es decir, la función que quieres que el proceso ejecute.
target
Por defecto es None lo que significa que nada es llamado, es decir, el proceso se creará, se pondrá en marcha y terminará
inmediatamente porque no tiene instrucciones que seguir.
Solo tiene sentido dejarlo como None si estás usando herencia (creando tu propia clase) y escribes el código dentro
del método run() .
es el nombre del proceso, es una “etiqueta” que no afecta al funcionamiento del código. Si no pones nada, Python
name
les asigna nombres genéricos como Process-1 .
es una tupla de argumentos para esa función. Son los “datos de entrada” para la función que pongas en
args target . Si no
hay función ( None ), no hay a quién pasarle esos datos, así que no habría que pasar argumentos.
es un diccionario para los argumentos keyword de la invocación del
kwargs target . Es decir, es igual que args , pero para
funciones que reciben argumentos con nombre.
args se pasa como una lista/tupla: (10, 20)
kwargs se pasa como un diccionario: {"limite": 100, "color": "rojo"}
Si tu función es def saludar(nombre, edad): , puedes usar args=("Juan", 25) o kwargs={"nombre": "Juan", "edad": 25} .
daemon . Aquí hay tres estados posibles:
: El proceso hijo es un "esclavo" del principal. Si el programa principal termina (o lo cierras), el proceso hijo se
True
muere al instante, aunque no haya terminado su trabajo.
: El proceso hijo es independiente. Si el programa principal termina, el proceso hijo seguirá vivo hasta que
False
complete su tarea.
(Heredar): Significa que el hijo copiará lo que sea que sea el padre. Si el proceso principal no es demonio, el hijo
None
tampoco lo será. Significa "lo que diga mi padre".
Por defecto, no se pasan argumentos al target (la función que queremos ejecutar). El argumento args (que por defecto es
una tupla vacía () ) se usa para especificar la lista o tupla de argumentos posicionales (en orden) para el target . Es decir, si la
función necesita datos hay que introducirlos en una lista o tupla en args para que no de un error.
Procesos en Python 1
Si una subclase sobrescribe el constructor, debe asegurarse de invocar al constructor de la clase base ( super().__init__() )
antes de hacer cualquier otra cosa con el proceso.
En Python, cuando heredas de Process , tu nueva clase es como un "hijo" que quiere ser un proceso. Para que ese hijo tenga
todas las herramientas necesarias (como el número de identidad pid o la capacidad de comunicarse con el sistema
operativo), necesita que la "madre" (la clase Process original) haga su trabajo de configuración primero. Para eso ponemos el
super().__init__() .
En general, todos los argumentos que le pases a un Process deben ser "picklables".
Pickable → “algo desmontable” que pueda enviarse, por ejemplo, números, cadenas de texto, funciones y clases que estén
escritas en un archivo .py, etc. ¿Que no es pickable? No son pickables archivos abiertos, conexiones a internet o bases de
datos, funciones que se escriben directamente en la consola (REPL), funciones locales, etc.
REPL → Consola / Intérprete de Python
Si pasamos argumentos no pickables obtendremos errores, esto se debe a que cada proceso tiene su propio espacio de
memoria independiente, no pueden simplemente "compartir" un objeto. Si el Proceso A tiene una manzana (un dato) y quiere
que el Proceso B la use. Como no pueden tocarse, el Proceso A tiene que:
1. Desmontar la manzana pieza por pieza (convertirla en bytes). A esto se le llama Pickle.
2. Enviar esos bytes por un "tubo" al Proceso B.
3. El Proceso B recibe los bytes y vuelve a montar la manzana. A esto se le llama Unpickle.
Si intentas enviar algo que Python no sabe cómo "desmontar" (unpickable), el programa fallará.
Pasar una función definida en la sesión actual de la consola (REPL) hace que el proceso hijo muera con un error
AttributeError .
Cuando el proceso hijo intenta "volver a montar" la función (unpickling), necesita saber de dónde salió. El hijo dice: "Vale, me
han mandado una función llamada knigit , voy a buscarla en el archivo principal".
Si estás usando un archivo .py , el hijo lo lee y la encuentra.
Si estás escribiendo directamente en la consola (REPL), el hijo no tiene un archivo que leer y dice: AttributeError: module
'__main__' has no attribute 'knigit' .
run() → Es el método que representa la actividad del proceso. Puedes sobrescribir este método en una subclase. El método
run() estándar invoca al objeto (función) que pasaste en el constructor como argumento target , usando los argumentos de
args y kwargs .
Hay dos formas de decirle a un proceso qué tiene que hacer:
1. Forma Directa (sin clases): Si creas un Process(target=funcion, args=(1,)) , el método run() por defecto simplemente agarra
esa funcion y la ejecuta con el 1 .
2. Forma con Herencia (con clases): Si creas tu propia clase (como ProcesoEstupendo en tus apuntes), "sobrescribes"
(cambias) el método run() para escribir ahí dentro directamente el código que quieres que se ejecute.
start() → Inicia la actividad del proceso. Esto debe llamarse como máximo una vez por cada objeto de proceso. Se encarga
de que el método run() del objeto sea invocado en un proceso separado.
Cuando haces [Link]() , el sistema operativo crea un clon de tu programa (un nuevo pid ) y en ese nuevo espacio ejecuta el
método run() .
Si intentas llamar a start() dos veces sobre el mismo objeto p , Python te dará un error. Si quieres ejecutar lo mismo otra vez,
tienes que crear un objeto nuevo.
No bloquea: A diferencia de run() , cuando llamas a start() , el código de tu programa principal sigue adelante
inmediatamente mientras el hijo trabaja en paralelo.
Es decir, run() sí hace el trabajo, pero NO crea el paralelismo. Si tú en tu código escribes [Link]() , Python lo ejecuta como una
función normal y corriente. El programa se detendrá ahí, ejecutará todo lo que haya dentro de run() y, cuando acabe, pasará
a la siguiente línea. No hay dos cosas pasando a la vez.
[Link]() → Ejecuta el código en el mismo proceso (secuencial, aburrido).
[Link]() → Le dice al Sistema Operativo: "Crea un proceso nuevo y, dentro de ese nuevo proceso, ejecuta el método
run() ".
Procesos en Python 2
join( [timeout] ) → Si el argumento opcional timeout es None (por defecto), el método bloquea el programa principal hasta
que el proceso al que join() se llamó termine. Si el timeout es un número positivo, se bloquea como mucho esa cantidad de
segundos.
Cuando el proceso principal llega a la línea [Link]() , se queda "congelado" ahí. No pasará a la siguiente línea hasta que el
proceso p haya terminado su trabajo.
Es fundamental para evitar que el programa principal intente imprimir resultados antes de que los procesos hijos hayan
acabado de calcularlos.
Si no quieres esperar eternamente a un proceso que se ha quedado colgado, puedes decirle [Link](5) . Esperará 5 segundos;
si el proceso termina, genial, y si no, el programa principal sigue adelante de todos modos.
Tenga en cuenta que el método devuelve None si su proceso finaliza o si se agota el tiempo de espera del método. Es decir, si
haces resultado = [Link](2) la variable resultado = None . Para saber si qué ocurrió realmente después de un join() con un
tiempo límite tienes que comprobar el exitcode del proceso. Si [Link] = None el proceso sigue vivo, el join() se desbloqueó
porque se acabo el tiempo (timeout), no porque el proceso terminara. Si [Link] es un número el proceso ya terminó.
Puedes llamar a join() varias veces sobre el mismo proceso sin error.
Un proceso no puede intentar esperarse a sí mismo porque causaría un deadlock o interbloqueo.
No puedes esperar a alguien que ni siquiera ha empezado: primero start() , luego join() .
name → Es un nombre para identificar el proceso. No afecta al código. Se puede asignar el mismo nombre a varios procesos.
El nombre inicial lo establece el constructor.
is_alive() → Devuelve si el proceso está vivo. Un proceso está "vivo" desde que haces start() hasta que termina su función
run() .
daemon → El indicadordel proceso es un valor booleano. Debe establecerse antes de llamar a start() . El valor inicial se
daemon
hereda del proceso que lo crea. Cuando un proceso termina, intenta finalizar a todos sus procesos hijos que sean daemon .
Proceso No-Demonio (Normal): El programa principal no terminará hasta que este proceso acabe su tarea.
Proceso Demonio ( True ): Es un proceso "esclavo" o de apoyo. Si el programa principal termina (o lo cierras), el proceso
demonio muere inmediatamente, sin importar si ha terminado su trabajo o no.
Si tu proceso principal no es un demonio (que es lo normal), sus hijos tampoco lo serán por defecto. Un proceso demonio no
tiene permitido crear sus propios procesos hijos. Esto evita que queden "procesos huérfanos" en el sistema cuando el padre
muera.
A los procesos demonios no se les suele hacer join() (esperar por ellos), porque precisamente su gracia es que mueran
solos cuando el programa principal decida que ya ha terminado.
pid → Devuelve el ID (es temporal) del proceso. Antes de que el proceso sea generado, esto será None , es decir, no existe
hasta hacer [Link]() .
exitcode → Es como el "informe final" que el proceso hijo le entrega al sistema operativo al terminar. Es fundamental para
saber si todo salió bien o si hubo un error catastrófico.
Mientras trabaja: El valor es None . Si consultas [Link] y recibes None , significa que el proceso todavía está en
ejecución.
Éxito (Valor 0 ): Si el método run() del hijo termina normalmente (llega al final del código sin errores), el código de salida
será 0 .
Error por excepción (Valor 1 ): Si el proceso hijo sufre un error (una excepción no capturada como una división por cero),
el proceso "muere" y deja un exitcode de 1 .
Terminación forzada (Valores negativos): Si el proceso fue "asesinado" por una señal externa (por ejemplo, si tú llamas a
terminate() ), el código de salida será un número negativo (como -15 o -9 ). El signo menos indica que el proceso no
terminó por su cuenta, sino que lo obligaron.
authkey → Es una "contraseña / clave de autenticación" interna (una cadena de bytes) que usa Python para asegurar que los
procesos que intentan comunicarse entre sí realmente pertenecen al mismo programa. Por defecto, el proceso principal
genera una clave aleatoria y todos los procesos hijos la heredan automáticamente.
sentinel → Un identificador numérico de un objeto del sistema que estará "listo" al finalizar el proceso. Usar join() es mucho
más sencillo y preferible.
Procesos en Python 3
interrupt() → Envía una señal para interrumpir el proceso (como pulsar Ctrl+C). El proceso hijo puede capturar esta
interrupción y decidir si cerrar ordenadamente o ignorarla.
terminate() → Es más contundente, envía una orden de terminación inmediata. Detiene el proceso inmediatamente usando
señales del sistema operativo. Ojo: No ejecuta las cláusulas finally ni los gestores de salida del hijo. Si el proceso que
terminas tiene a su vez procesos hijos, esos nietos no mueren. Se quedan "huérfanos" en el sistema operativo.
kill() → Lo mismo que pero usando la SIGKILL .
terminate()
Mientras que terminate() envía una señal que el sistema operativo intenta manejar de forma "limpia", kill() es una orden
fulminante. No hay ninguna posibilidad de que el proceso haga nada más que desaparecer.
La documentación advierte que si usas terminate() o kill() mientras el proceso está usando una Queue (cola) o un Lock
(cerrojo), podrías corromper esos datos y dejar el resto del programa bloqueado (deadlock).
close() → cierra el objeto liberando todos los recursos asociados. Se genera una excepción ValueError si el proceso
Process
subyacente sigue en ejecución, es decir, el proceso debe haber terminado antes de poder cerrarlo. Si intentas llamar a
close() mientras el proceso sigue vivo ( is_alive() es True ), Python lanzará un error de tipo ValueError . Una vez que close()
finaliza correctamente, la mayoría de los demás métodos y atributos del objeto Proccess generarán una excepción ValueError .
Tenga en cuenta que los métodos start() , join() , is_alive() , terminate() y exitcode solo deben ser invocados por el proceso
que creó el objeto.
Solo el "Padre" (el proceso que creó el objeto) tiene derecho a preguntar si el hijo vive, a esperarlo con join() o a matarlo.
Los procesos hijos no deben intentar usar estos métodos sobre sus propios hermanos o sobre su padre; el control debe ser
siempre jerárquico.
Excepciones (errores específicos) que pueden ocurrir cuando trabajas con Process.
multiprocessing. ProcessError → Es la excepción base. Casi todos los errores de este módulo descienden de aquí. Si quieres
capturar cualquier error relacionado con procesos en un bloque try/except , puedes usar esta clase.
multiprocessing. BufferTooShort → Está relacionado con la comunicación. Ocurre cuando intentas leer un mensaje que te
envía otro proceso, pero el "recipiente" (buffer) que has preparado es demasiado pequeño para que quepa todo el
contenido. Si capturas este error como e , puedes ver el mensaje original en [Link][0] como una cadena de bytes.
multiprocessing. AuthenticationError → Se genera cuando hay un error de autenticación. Cuando dos procesos intentan
conectarse o enviarse datos pero sus authkey no coinciden. Es una medida de protección para evitar interferencias de
procesos ajenos.
multiprocessing. TimeoutError → Se activa mediante métodos con un tiempo de espera cuando éste expira. Ocurre cuando
utilizas métodos que tienen un tiempo límite (como un join(timeout=5) o al esperar un resultado de un Pool ) y ese tiempo
se agota antes de que la tarea termine.
Esquema de uso.
[Link]
from multiprocessing import Process
class ProcesoEstupendo(Process):
def __init__(self) -> None:
super().__init__()
def run(self):
print("Soy un proceso estupendo.")
class ProcesoMasEstupendo(Process):
def __init__(self, valor: int) -> None:
super().__init__()
[Link] = valor
def run(self):
print(f"Soy un proceso más estupendo y he recibido el valor {[Link]}.")
Procesos en Python 4
if __name__ == "__main__":
pe = ProcesoEstupendo()
pme1 = ProcesoMasEstupendo(1)
pme2 = ProcesoMasEstupendo(2)
[Link]()
[Link]()
[Link]()
[Link]()
[Link]()
[Link]()
ProcesoEstupendo → en el constructor usa el super().__init__() para que todo funcione correctamente.
El método run() es lo que hará el proceso cuando se ejecute.
ProcesoMasEstupendo → los procesos pueden recibir valores de entrada.
pe = ProcesoEstupendo() o pme1 = ProcesoMasEstupendo(1) → Todavía no existe el proceso en el sistema operativo, solo es un
objeto en memoria.
[Link]() → es una orden al proceso hijo para que se ponga en marcha. Es ahora cuando el Sistema Operativo crea
p
el proceso y ejecuta el run() de este objeto.
[Link]() → es una orden al padre para que espere.
En el código vemos como se crean tres procesos hijos independientes. Los tres empiezan a funcionar a la vez, pero cada
uno por su cuenta.
Al hacer [Link]() lo que hace es que el padre espere a los hijos, es decir, hasta que no se termine de ejecutar p no pasa a
la siguiente línea de código. En nuestro ejemplo:
1. [Link]() , [Link]() , [Link]() : Pegas el pistoletazo de salida. Los tres hijos empiezan a la vez.
2. [Link]() : Tú (el Padre) esperas a que acabe exclusivamente pe .
Si pme1 y pme2 acaban mientras tú esperas a pe , a ti te da igual, tú sigues esperando a pe .
No pasarás a la siguiente línea de código hasta que pe acabe.
3. [Link]() : Una vez que pe acaba, pasas a la siguiente línea, que te manda a esperar a pme1 , miras a ver si pme1 ya ha
acabado:
Si pme1 ya había acabado mientras esperabas a pe , este join() se ejecuta en un milisegundo (no esperas nada).
Si pme1 aún no ha acabado, vuelves a ponerte a esperar.
4. [Link]() : Lo mismo para el último.
La clave es: Ponemos todos los join() seguidos para obligar al Padre a que no termine el programa hasta que todos (uno
por uno) hayan acabado.
Cuidado, esto no es paralelismo:
[Link]()
[Link]()
[Link]()
[Link]()
Al llamar a join() inmediatamente después del primer start() , el proceso principal se bloquea y se queda esperando. Por
tanto, p2 ni siquiera se crea hasta que p1 ha muerto por completo, resultando en una ejecución puramente secuencial.
Otra forma de uso [Link]
from multiprocessing import Process
class ProcesoQueEscribeUnCuadrado(Process):
def __init__(self, num: int) -> None:
Procesos en Python 5
super().__init__()
[Link] = num
def run(self):
cuad = [Link] * [Link]
print(f"El cuadrado de {[Link]} es {cuad}.")
if __name__ == "__main__":
procesos = [ProcesoQueEscribeUnCuadrado(n) for n in range(10)]
for p in procesos:
[Link]()
for p in procesos:
[Link]()
Este bloque de código muestra cómo gestionar muchos procesos a la vez.
Primero crea 10 objetos de proceso a la vez mediante una lista (objetos de memoria del programa principal, todavía no se
ha creado nada real).
Con el primer bucle for lanza mediante .star() todos ellos a la vez (prácticamente). Así se ponen a trabajar todos en
paralelo.
Con el segundo bucle for hace que el proceso padre recorra la lista esperando a que acabe cada proceso hijo. Si por
ejemplo, el proceso número 5 termina antes que el número 1, el padre seguirá esperando al 1 en la primera iteración del
bucle, pero cuando llegue el turno de hacer join() al número 5, pasará de largo porque ya habrá terminado.
Es parecido a [Link] , pero no puede devolver valores (fácilmente), pues [Link] devuelve una lista de resultados
directamente. La clase Process está diseñada para ejecutar una tarea y terminar. Si quieres que un Process te devuelva
un dato, necesitarás herramientas más avanzadas como las Colas (Queue) o la Memoria Compartida.
Procesos lanzados en paralelo pueden acabar en cualquier orden.
Procesos hijos de los hijos:
hijos_y_nietos.py
from multiprocessing import Process
class Hijo(Process):
def __init__(self) -> None:
super().__init__()
def run(self):
print("soy un hijo")
nietos = [Nieto() for _ in range(3)]
for n in nietos:
[Link]()
for n in nietos:
[Link]()
class Nieto(Process):
def __init__(self) -> None:
super().__init__()
def run(self):
print("soy un nieto")
if __name__ == "__main__":
hijo = Hijo()
Procesos en Python 6
[Link]()
[Link]()
El PADRE es el programa principal, este crea un hijo = Hijo() y lo lanza con start() , después se queda esperando con join() .
Cuando el método run() de hijo empieza este se comporta como el proceso principal, crea 3 nuevos procesos Nieto , los
lanza a todos a la vez y luego espera a que todos terminen.
Los Nietos solo imprimen un mensaje.
Es importante ver cómo se detiene la ejecución en cada nivel:
1. El Padre se detiene en [Link]() . No puede avanzar hasta que el Hijo muera.
2. El Hijo está "vivo", pero se detiene en sus propios [Link]() . No puede terminar su método run() hasta que sus 3 Nietos
mueran.
3. Los Nietos terminan su trabajo.
4. Solo cuando el último Nieto termina, el Hijo puede terminar su run() .
5. Solo cuando el Hijo termina, el Padre puede seguir adelante después de su [Link]() .
En la documentación decía que un proceso daemon no puede tener hijos:
Si hubieras configurado [Link] = True , este código daría un error al intentar crear a los nietos.
Como no hemos dicho nada, por defecto daemon es False , así que el Hijo tiene permiso legal para "ampliar la familia".
En la práctica es mejor que cada clase vaya en un archivo .py distinto:
Archivo [Link] : Contiene la clase Nieto .
Archivo [Link] : Hace from nieto import Nieto y contiene la clase Hijo .
Archivo [Link] : Hace from hijo import Hijo , tiene el bloque if __name__ == "__main__": y lanza al primer hijo.
Notas finales
Cada proceso se ejecuta en su propio espacio de memoria (contexto). A diferencia de los hilos, aquí las variables globales
no se comparten; si un proceso modifica una lista global, los demás no verán ese cambio porque están operando sobre su
propia copia. Esta es la razón por la que entre procesos compartir información es mucho más costoso.
Siempre existe un proceso principal. Al igual que en las hebras hay una principal, siempre existe un proceso principal en
cada programa.
Comunicación y sincronización entre procesos:
Envío de mensajes entre procesos → [Link]
Compartición de memoria → [Link] y [Link]
Sincronización → [Link] , [Link] ,…
[Link] está construido sobre estas herramientas.
Procesos en Python 7