Procesos
Ejecutor
Un runtime ejecuta sus procesos en sus hilos de trabajo del planificador (hilos de trabajo, modelo de ejecución). El arranque convierte la llamada a la función de entrada en el primer proceso, el proceso principal, y ejecuta el ejecutor hasta que este termina:
- Los procesos ejecutables esperan en la cola de ejecución FIFO (el primero en
entrar es el primero en salir) de su hilo de trabajo
(hilos de trabajo). Un hilo de trabajo ejecuta el primero durante
una porción de tiempo de 4.000 reducciones (
CONTEXT_REDSde OTP) y después lo devuelve al final de su cola, salvo que haya terminado. - Cada entrada de función gasta una reducción: llamadas, llamadas de cola, llamadas a funs y dinámicas, y builtins. Si no queda ninguna, la entrada no ocurre: el proceso registra la función en la que iba a entrar, conserva sus argumentos en sus registros (siguen siendo raíces de recolección) y vuelve al ejecutor, que más tarde lo reanuda repitiendo la entrada (modelo de ejecución). Cada cabecera de bucle de una comprehension también gasta una, de modo que un bucle sin llamadas también cede el control. Los builtins cuyo trabajo crece con un argumento lista o binary se ejecutan por porciones y ceden el control entre ellas (porciones).
- Cada proceso posee su heap y su pila. Los argumentos de un proceso nuevo se copian en su heap (copia entre heaps).
- Un proceso termina cuando su primera llamada retorna o lanza una excepción.
Un proceso terminado sale de la tabla de procesos de inmediato; su pid sigue
siendo un término válido y
is_process_alive/1devuelve false para él. Su contexto, heap y pila se liberan cuando el hilo de trabajo que lo terminó sale del mutex del ejecutor. - Un proceso que espera en un
receiveno está en ninguna cola; un envío dirigido a él lo vuelve a encolar (receive). - Cuando el proceso principal termina, el programa termina con su resultado
(ejecutables): los procesos que siguen en la cola o en
espera se liberan sin seguir ejecutándose, igual que un escript de OTP se
detiene cuando
main/1retorna. - Una señal de salida que termina el proceso principal termina el programa (señales de salida).
erlang:halt/0,1en cualquier proceso termina el programa con su estado. Un fallo del runtime en cualquier proceso (memoria agotada, un límite opcional superado, un error interno) termina el programa como fallo del runtime (salida 70). Una excepción de Erlang solo termina el proceso que la lanzó (salidas).- Las invocaciones desde el anfitrión de funciones exportadas
(
CLAUSE_invoke_v1) ejecutan su función en el contexto que llama hasta el final, reanudándola tras cada cesión sin ejecutar otros procesos; los programas arrancados porCLAUSE_main_v1usan el ejecutor.
Hilos de trabajo
El ejecutor ejecuta los procesos en RuntimeOptions::schedulers hilos de
trabajo: el hilo que arrancó el programa y un hilo más por cada hilo de trabajo
adicional. Los programas toman el número de --schedulers N (de 1 a 1.024,
opciones del runtime); por defecto hay un
hilo de trabajo por procesador lógico en el que el programa puede ejecutarse
(su máscara de afinidad, como los planificadores en línea de OTP), o por CPU de
--scheduler-cpus.
- Cada hilo de trabajo tiene su propia cola de ejecución, como cada planificador de ERTS: un proceso se queda con el hilo de trabajo en el que se ejecutó por última vez, de modo que su heap permanece en las cachés de ese núcleo. Un proceso lanzado empieza en el hilo de trabajo de su padre. Los hilos de trabajo inactivos duermen hasta que se encola un proceso para ellos o hay trabajo que robar; el hilo de temporizadores encola los procesos cuyo tiempo de espera de receive vence (tiempos de espera de receive).
- Los procesos migran como en ERTS, que equilibra sus colas de ejecución periódicamente: cada 4 porciones un hilo de trabajo compara su cola con la de su vecino y toma un proceso de ella cuando el vecino tiene al menos dos más. Un hilo de trabajo con la cola vacía roba un proceso de la cola más larga que tenga al menos dos, o el único proceso de un hilo de trabajo dormido, que ejecuta antes de que ese hilo despierte.
- Los despertares son perezosos, igual que ERTS mantiene en un mismo planificador los procesos que se comunican entre sí: un proceso en ejecución que despierta a un proceso de un hilo de trabajo dormido y vacío lo encola en su propio hilo de trabajo, que lo toma cuando termina su porción; solo se despierta a otro hilo de trabajo cuando una cola pasa a tener más de un proceso. Los hilos de temporizadores y de E/S, las tareas de puertos y los hilos de trabajo entre porciones encolan un proceso para su propio hilo de trabajo, o para uno inactivo cuando el suyo está ocupado.
- Un mutex del ejecutor protege los procesos en espera, los temporizadores, los nombres registrados, los enlaces y monitores de todos los procesos y todo proceso que no esté en ejecución. El heap, la pila, el buzón y el canal de fallos de un proceso en ejecución pertenecen solo a su hilo de trabajo; el código Erlang se ejecuta sin el bloqueo. Las colas de ejecución y el estado inactivo de los hilos de trabajo tienen un bloqueo propio, que se toma después del mutex del ejecutor; los hilos de trabajo inactivos duermen sobre él (creación y desmontaje de procesos).
- Un spawn construye el contexto del nuevo proceso y copia en él su llamada
sin ningún bloqueo. Si no enlaza ni monitoriza, después lo publica en la
tabla de procesos y lo encola solo bajo los bloqueos propios de estas, sin
tomar nunca el mutex del ejecutor;
spawn_link,spawn_monitory las formas despawn_opt/spawn_requestcon monitorización publican, enlazan o monitorizan y encolan bajo el mutex del ejecutor. Un hilo de trabajo libera los contextos de los procesos terminados justo después de salir del mutex para su siguiente porción. - Un proceso cuesta seis asignaciones durante su vida (coste de creación de procesos): su contexto, los enlaces del contexto con el runtime junto con su estado de planificación, su almacenamiento de heap y primer bloque de heap, sus llegadas y su testigo de vida. Las primeras 64 palabras de pila y cuatro nodos de lista del buzón viven dentro del contexto, los maps de monitores se crean con el primer monitor, y los registros del runtime (contextos, recuperación de términos persistentes) no conservan un nodo por proceso.
- Los enlaces, los monitores y los nombres cambian bajo el bloqueo de
inmediato, también para un proceso que se ejecuta en otro sitio. Un mensaje
espera en un búfer entre las llegadas del receptor hasta que este lo toma
(búferes de llegadas). Una señal de salida
de
exit/2oexit_signal/2dirigida a un proceso así no hace nada y su proceso termina su porción (builtins::Blocked); espera a que termine la porción del destino, después se ejecuta el primero y repite el builtin con los mismos argumentos. El destino queda retenido hasta entonces, así que el builtin repetido no puede volver a encontrarlo en ejecución. Un proceso que termina con enlaces o monitores cuyos procesos se ejecutan en otro sitio se finaliza del mismo modo cuando terminan sus porciones. - Así, las señales conservan su orden: un mensaje almacenado antes de una señal
de salida bloqueada del mismo emisor llega primero al receptor, e
is_process_alive/1después deexit(Pid, kill)es false, como promete OTP para las señales del llamante. - Un spawn enlaza o monitoriza el proceso nuevo (
spawn_link,spawn_monitor) antes de que ningún hilo de trabajo pueda ejecutarlo. Un proceso lanzado puede ejecutarse antes de que su padre continúe, así que un programa que monitoriza o se enlaza a un proceso de vida corta después despawn/1puede vernoproc, como en un OTP con varios planificadores. - El final del proceso principal, un halt o un fallo del runtime detienen el programa en cuanto todos los hilos de trabajo han terminado su porción actual; después se liberan los demás procesos.
- Los servicios compartidos del runtime están sincronizados (hilos): los átomos, el servidor de código, los números de pid y de puerto y la cuenta de memoria.
- La salida de distintos procesos se intercala en el orden en que ocurren sus
escrituras. Cada llamada a
io:formaty aerlang:displayescribe su texto de una vez.
Los despertares y el apagado no necesitan ningún mecanismo adicional, porque todo cambio del estado de planificación de un proceso ocurre bajo el mutex del ejecutor:
- Un mensaje,
'EXIT'o'DOWN'para un proceso en espera lo encola en la misma sección crítica que lo entrega, y despierta a un hilo de trabajo como se ha descrito. Un proceso que sigue en la porción en la que empezó a esperar no puede recibir un mensaje en ese momento (sus emisores esperan a que termine la porción), así que la entrega siempre lo encuentra aparcado. Tampoco se pierde un mensaje depositado (búferes de llegadas): al aparcar se comprueban las llegadas bajo su bloqueo, y un depósito que encuentra al receptor aparcado lo despierta bajo el mutex del ejecutor. - Un hilo de trabajo que no encuentra ningún proceso ejecutable duerme hasta que otro hilo de trabajo, un envío, un spawn o el hilo de temporizadores encola uno, o hasta el final del programa; consulta las colas y duerme bajo el bloqueo de las colas de ejecución, que toma toda operación de encolado. Los hilos de trabajo nunca leen el reloj para los temporizadores; aparcar un proceso con un tiempo de espera anterior a todos los demás solo despierta al hilo de temporizadores.
- Un mensaje y el vencimiento del tiempo de espera de un mismo receive pueden competir: el que llegue primero encola el proceso y cancela al otro, y el receive toma un mensaje que llegó antes de reanudarse, como hace OTP.
- Una señal de salida dirigida a un proceso en espera, encolado, retenido o bloqueado lo saca de donde esté antes de finalizarlo.
- Cuando el programa termina, cada hilo de trabajo termina su porción actual y se
detiene;
run()retorna tras esperarlos (join), y todos los demás procesos que inició el ejecutor se liberan, de modo que el runtime se apaga sin que quede ningún contexto. - El golden de OTP
executables_wakeupspone esto a prueba con 1, 2, 4 y todos los hilos de trabajo: receptores cuyos tiempos de espera de 0–2 ms compiten con los mensajes de sus emisores, una cadena de 16 procesos enlazados en bucle activo terminada por una única señal de salida con cada miembro monitorizado, procesos monitorizados que terminan mientras su observador despierta, un programa que termina mientras los procesos giran, esperan y se inundan entre sí, y un halt en otro proceso.
Afinidad de CPU
Por defecto, los hilos de trabajo no están vinculados: el SO los sitúa en
cualquier CPU de la máscara de afinidad del programa, como el valor por defecto
+sbt u de OTP. --scheduler-cpus SET
(opciones del runtime) vincula el hilo de
trabajo N a la CPU N del conjunto, por turnos cuando hay más hilos de trabajo,
como +sbt/+sct de OTP; los hilos de temporizadores y de E/S siguen sin
vincularse.
performancees la clase de núcleos más rápida: los núcleos de Windows con la mayorEfficiencyClass,/sys/devices/cpu_core/cpusen Linux (Intel híbrido) o las CPU con la mayorcpu_capacity; en una CPU cuyos núcleos son iguales, todas las CPU.allson todas las CPU de la máscara de afinidad. Una lista (0-15,2,4,6, CPU hasta 1023) debe nombrar solo CPU de la máscara de afinidad:--scheduler-cpus: CPU N is not available to the program, salida 70.- Windows numera las CPU del grupo de procesadores del proceso (como máximo
64);
performancelee el grupo 0. - macOS no tiene afinidad de hilos: la opción se acepta,
performancecuenta el primer nivel de rendimiento (hw.perflevel0.logicalcpu) para el número por defecto de hilos de trabajo, y la vinculación solo eleva cada hilo de trabajo a la clase de QoSUSER_INTERACTIVE, que macOS atiende primero en los núcleos P.
Tiempos de espera de receive
Los temporizadores viven en una rueda de temporizadores jerárquica con hash
(timer wheel), como los mantiene ERTS (scheduler/timer_wheel):
- Los ticks son milisegundos desde que arrancó el ejecutor. Seis niveles de 64 ranuras cubren 2^36 ticks, más que el tiempo de espera más largo (4.294.967.295 ms). Un temporizador se sitúa en la ranura del dígito de 6 bits más alto en el que su tick difiere del tick actual de la rueda; las ranuras ocupadas son mapas de bits por nivel.
- Armar (se aparca un proceso en espera) y cancelar (llega antes un mensaje, una señal de salida o el final del proceso) desenlazan o enlazan un nodo de lista intrusiva guardado con el estado de planificación del proceso: tiempo constante. Un plazo se convierte en el tick igual o posterior a él, de modo que ningún tiempo de espera se dispara antes de tiempo.
- Un hilo de temporizadores por ejecución duerme hasta la siguiente ranura ocupada (la primera ranura ocupada después de la actual en cualquier nivel), lee el reloj una vez al despertar y avanza la rueda hasta ese tick: las ranuras de niveles superiores alcanzadas en su inicio bajan sus temporizadores, y la ranura de nivel 0 se dispara. Solo se consultan las ranuras cuyo momento ha llegado; los procesos en espera nunca se recorren. Los procesos vencidos se encolan en orden de tick.
- Los hilos de trabajo comparten la rueda bajo el mutex del ejecutor.
after 0nunca espera einfinitynunca arma un temporizador. --profileinforma de las porciones de tiempo y de las lecturas del reloj del hilo de temporizadores (perfilado). Los temporizadores solo se disparan con la precisión de las esperas temporizadas del anfitrión (en Windows, la resolución de temporizador por defecto).runtime_timer_wheelarma 20.000 temporizadores en todos los niveles, cancela el 90% y avanza en pasos desiguales: cada temporizador restante se dispara una vez, en el primer avance que alcanza su tick, en orden de tick; ninguno cancelado se dispara.
Salidas
Un proceso distinto del principal termina con una razón de salida, como en OTP
(detail::exit_reason, process/exits). Las señales de salida la llevan a los
procesos enlazados (enlaces) y los mensajes 'DOWN' a los que lo
monitorizan (monitores); los informes de error la muestran.
| Cómo termina el proceso | Razón de salida | Informe de error |
|---|---|---|
| Su primera llamada retorna | normal | Ninguno |
exit(Reason) (también normal, kill) | Reason | Ninguno |
Un error (error/1,2,3, badarith, {badmatch, V}, undef, ...) | {Reason, Stack} | Sí |
Un throw(Value) no capturado | {{nocatch, Value}, Stack} | Sí |
| Lo termina una señal de salida (señales de salida) | La razón de la señal, killed para exit(Pid, kill) | Ninguno |
Un informe de error se escribe en stderr cuando el proceso termina, después de vaciar la salida estándar, con el formato del manejador de logger por defecto de OTP:
=ERROR REPORT==== 8-Oct-2026::03:42:15.983000 ===
Error in process <0.8.0> with exit value:
{boom,[{crash_reports,'-main/1-fun-6-',0,[]}]}
La cabecera lleva la hora local; la razón se presenta como lo hace ~p. El
proceso principal no escribe ninguno: su excepción no capturada es la del
programa (ejecutables).
Mensajes
Dest ! Msg y erlang:send(Dest, Msg) evalúan primero Dest y devuelven
Msg.
Dest | Efecto |
|---|---|
| El pid de un proceso vivo | Msg se copia de forma plana en el heap del receptor (copia entre heaps), como OTP copia los mensajes (uno grande, en un bloque que el receptor asume, traspaso de mensajes), y se añade a su buzón de señales; para un receptor que se ejecuta en otro hilo de trabajo, en un búfer entre sus llegadas que toma la próxima vez que las consulta (búferes de llegadas) |
| El pid de un proceso que ha terminado | Nada; el envío tiene éxito |
| Un nombre registrado | Como para su pid; badarg cuando ningún proceso vivo tiene ese nombre |
{Name, nonode@nohost} de dos átomos | Como para el pid registrado como Name; nada si no hay ninguno |
{Name, Node} para cualquier otro nodo | Nada (no hay otros nodos) |
| Una referencia | Como para el propietario de un alias activo; nada en otro caso |
| Cualquier otra cosa | badarg |
- Los mensajes de un mismo emisor llegan en el orden en que los envió; un envío a sí mismo es un mensaje como cualquier otro.
- Cada mensaje es una raíz de recolección de su receptor hasta que un receive lo toma (heap del runtime).
- El buzón del receptor (
Mailbox) mantiene un buzón de señales, al que se añaden los envíos, y una cola de mensajes que receive recorre desde una posición guardada: los mensajes llegados pasan al final de la cola cuando un receive los examina. Ambas listas reutilizan hasta 64 nodos de reserva del buzón, de modo que el envío y la recepción de un mensaje no asignan ningún nodo del heap de C++. - Cuando el heap del receptor rechaza la copia (un límite opcional como
--max-heap, o el anfitrión sin memoria), no se entrega nada y el proceso emisor falla con un fallo del runtime (salida 70), como cualquier proceso que supera un límite; un receptor que rechaza un mensaje de sus llegadas falla él mismo.
Búferes de llegadas
Un envío a un proceso que se ejecuta en otro hilo de trabajo no toma ningún bloqueo (creación y desmontaje de procesos):
- La tabla de procesos del runtime (
ProcessTable) es un array de direccionamiento abierto de slots atómicos que un envío en un hilo de trabajo lee sin ningún bloqueo: los números de pid nunca se reutilizan, así que un slot borrado conserva una marca de borrado hasta que una inserción reconstruye el array. Las inserciones y los borrados toman un bloqueo de escritura y también mantienen el bloqueo del ejecutor, bajo el cual las búsquedas no necesitan nada más. - Cada proceso tiene sus llegadas (
Arrivals): una lista sin bloqueos en la que los emisores insertan mensajes, con una copia del estado de ejecución del proceso en el ejecutor. El emisor busca las llegadas del receptor y lee ese estado; un emisor cuyo último envío al mismo receptor lo encontró sin ejecutarse omite la búsqueda. - El emisor recorre el mensaje una vez, antes de cualquier bloqueo
(un solo recorrido del mensaje). Para un
receptor en ejecución, copia el mensaje justo detrás de un nodo de lista, en
una única asignación (un mensaje grande o uno con binaries fuera del heap, en
un
MessageBuffer), y lo inserta; el receptor copia después esas palabras a su heap y reubica sus punteros. Cualquier otro receptor recibe el mensaje bajo el bloqueo del ejecutor como antes: copiado en su heap, o insertado detrás de sus llegadas si entretanto empezó una porción o aún tiene llegadas, de modo que los mensajes de un emisor nunca se adelantan entre sí y el emisor nunca copia mensajes acumulados bajo el bloqueo. Cualquiera de los dos resultados de una carrera es correcto. - El receptor incorpora sus llegadas a su heap y buzón de señales, de más
antigua a más reciente: de 64 en 64 cuando su receive ha examinado todos los
mensajes y fuera a esperar, de modo que una avalancha llega al heap tan rápido
como la consume el receive (OTP conserva esos mensajes en fragmentos de heap
hasta que se necesitan); todas cuando hiberna, y antes de que se le entregue
cualquier otra cosa bajo el bloqueo del ejecutor (un mensaje,
'EXIT','DOWN', mensaje de puerto o de alias), de modo que los mensajes de cada emisor conservan su orden respecto a sus otras señales.process_infolas toma antes de contar o listar mensajes. Los mensajes de temporizadores y otros mensajes para un proceso en ejecución también esperan entre las llegadas. - Un proceso se aparca bajo el bloqueo del ejecutor solo tras una comprobación secuencialmente consistente de que no hay llegadas en espera; una inserción que lo encuentra aparcado toma el bloqueo del ejecutor y lo despierta.
- Las llegadas de un proceso terminado y los arrays de tabla sustituidos solo se liberan una vez que cada hilo de trabajo ha pasado un límite de porción después de su retirada, o espera trabajo (recuperación en reposo): un hilo de trabajo registra la época de recuperación en cada límite de porción, y un envío retiene lo que encontró solo dentro de su porción.
- Un envío a sí mismo va directamente al buzón de señales del propio emisor.
Receive
receive selecciona entre sus cláusulas como case, sobre los mensajes del
buzón:
- El recorrido del buzón empieza en el mensaje más antiguo. Cada mensaje se
compara con las cláusulas en orden (patrones y guards, que pueden leer
ligaduras anteriores al
receive); el primer mensaje que encaja con alguna cláusula se retira y se ejecuta el cuerpo de esa cláusula. Los mensajes que no encajan con ninguna cláusula se quedan en el buzón en su orden; el siguiente receive vuelve a empezar por el mensaje más antiguo. - Cuando se han examinado todos los mensajes, el proceso espera
(
CLAUSE_wait_frame_v1, un builtin en el que se entra como en una llamada): sale de la cola de ejecución hasta que un envío le entrega un mensaje, y entonces el recorrido continúa con los mensajes que han llegado. Los procesos en espera conservan sus marcos y mensajes como raíces de recolección y recolectan al reanudarse (procesos en espera). - Los nombres ligados en todas las cláusulas, y en el cuerpo de
aftercuando lo hay, se exportan después delreceive, como encase; una llamada en posición de cola del cuerpo de una cláusula o deafteres una llamada de cola, así que un bucle de servidor se ejecuta con pila constante. after T -> Body:Tse evalúa primero, antes del recorrido. Cuando el receive fuera a esperar,Tdebe serinfinityo un entero en 0..4294967295 (milisegundos); si no,error:timeout_value, tras lo cual el siguiente receive recorre desde el mensaje más antiguo; un mensaje que encaja de inmediato nunca lo comprueba, como en OTP.after 0ejecutaBodyen cuanto se han examinado todos los mensajes. Un tiempo de espera finito empieza cuando el receive espera por primera vez y no lo reinician los mensajes que no encajan con ninguna cláusula; cuando vence,Bodyse ejecuta a partir de las ligaduras anteriores al receive y el siguiente receive recorre desde el mensaje más antiguo. Un mensaje que llega antes de que venza el tiempo de espera se toma. Un receive con soloafteres una pausa (el modismo detimer:sleep/1).- Código generado: una cabecera de bucle consulta el siguiente mensaje sin
examinar (
peekdeCLAUSE_receive_v1, en una ranura raíz), la selección de cláusula toma un mensaje que encaja (take) antes de su cuerpo u omite uno que no encaja (skip) y repite el bucle; cuando no queda ningún mensaje, el bucle entra en la espera con el tiempo de espera, que respondetrue(recorrer de nuevo) ofalse(tiempo agotado:restarty después el cuerpo deafter). - El ejecutor mantiene un temporizador por cada proceso en espera con un tiempo de espera finito en una rueda de temporizadores (tiempos de espera de receive): uno vencido vuelve a encolar el proceso. Los tiempos de espera se miden en un reloj monótono en milisegundos y nunca se disparan antes de tiempo.
- Cuando todos los procesos esperan sin tiempo de espera un mensaje que nada
puede enviar, el programa espera para siempre, como hace OTP. Una invocación
desde el anfitrión (
CLAUSE_invoke_v1) que fuera a esperar falla en su lugar conbusy: ningún otro proceso se ejecuta durante ella.
Enlaces
Un enlace conecta dos procesos en ambos sentidos (Signals en cada contexto:
los pids enlazados en el orden de enlace y el indicador trap_exit).
link(Pid)enlaza al llamante con un proceso vivo y devuelvetrue; enlazarse consigo mismo o volver a enlazarse no hace nada. Para un proceso que ha terminado lanzaerror:noproco, cuando el llamante atrapa las salidas, devuelvetruey envía al llamante{'EXIT', Pid, noproc}, como hace ellink/1local de OTP.unlink(Pid)elimina el enlace en ambos lados; el enlace no tiene efecto después de que retorne. Devuelvetruetambién cuando no había enlace.spawn_link/1,3enlazan el proceso nuevo con el llamante antes de que se ejecute.- Cuando un proceso termina, cada proceso enlazado recibe una señal de salida con su razón de salida (salidas) y el enlace desaparece. Los procesos enlazados reciben la señal en el orden en que se crearon los enlaces.
Monitores
Un monitor es unidireccional: el proceso que monitoriza lo mantiene (por
referencia, en Signals) y el proceso monitorizado guarda la referencia y el
pid del que monitoriza, para poder enviar el mensaje cuando termine.
monitor(process, Pid)devuelve una referencia nueva. Cuando el proceso termina, el llamante recibe{'DOWN', Ref, process, Pid, Reason}con su razón de salida (salidas); para un proceso que ya ha terminado recibe el mensaje de inmediato con la razónnoproc. Monitorizarse a sí mismo no crea nada. Cada llamada crea un monitor independiente con su propio mensaje; los mensajes de un mismo proceso salen en el orden en que se crearon los monitores.demonitor(Ref)detiene el monitor: después no llega ningún'DOWN'suyo. Devuelvetrue, también para una referencia que no es un monitor activo del llamante.demonitor(Ref, Options):infodevuelve si el monitor seguía activo;flushelimina el mensaje{_, Ref, _, _, _}más antiguo cuando no lo estaba (su'DOWN'ya está en el buzón), como hace OTP.spawn_monitor/1,3devuelven{Pid, Ref}, el monitor creado antes de que se ejecute el proceso nuevo.- Un proceso que termina descarta los monitores que mantiene.
monitor(process, Name)ymonitor(process, {Name, nonode@nohost})monitorizan el proceso registrado comoName(noprocde inmediato si no hay ninguno); su'DOWN'nombra{Name, nonode@nohost}en lugar del pid.{Name, Node}para otro nodo esbadarg.monitor(port, Port | Name)monitoriza un puerto (puertos):{'DOWN', Ref, port, Port, Reason}cuando se cierra. Un pid paraport, o un puerto paraprocess, esbadarg.
Nombres registrados
El ejecutor mantiene una tabla de nombres (átomos) a pids, y cada proceso su
propio nombre (Signals::name).
register(Name, Pid)da nombre a un proceso vivo:badargparaundefined, un nombre en uso, un proceso que ya tiene nombre, un proceso que ha terminado o unPidque no es un pid. Un proceso puede registrarse a sí mismo.unregister(Name)libera el nombre (badargcuando ningún proceso lo tiene);whereis(Name)es el pid oundefined;registered()enumera los nombres en el orden de la tabla de átomos (el orden de OTP tampoco está especificado).- El nombre de un proceso se libera al terminar, antes de que se envíen las
señales a sus enlaces y monitores, así que un receptor de
'DOWN'o'EXIT'puede volver a registrar el nombre, como en OTP.
Diccionario del proceso
put/2, get/0,1, get_keys/0,1 y erase/0,1 se comportan como en OTP:
put/2 y erase/1 devuelven el valor anterior o undefined, erase/0
devuelve todos los {Key, Value} y vacía el diccionario, y las claves se
comparan con igualdad exacta (1 y 1.0 son dos claves).
- Cada proceso tiene su propio diccionario; un proceso lanzado empieza vacío.
- Las entradas son palabras del heap del proceso guardadas en
ProcessDictionary(process_dictionary.hpp) y visitadas como raíces de recolección (raíces); se liberan con el proceso. - El orden de
get/0yget_keys/0,1no está especificado, como en OTP. La búsqueda recorre las entradas linealmente; las claves inmediatas se comparan por palabra.put/2,get/1yerase/1toman y devuelven las palabras de las entradas tal cual, sin admitir ningún término.
Temporizadores
start_timer/3,4 y send_after/3,4 (builtins/erlang/timers.cpp,
scheduler/timers.cpp) arman un temporizador en la rueda del ejecutor (la
misma que usan los tiempos de espera de receive) que envía
Msg, o {timeout, TimerRef, Msg}, a un pid o a un nombre registrado cuando
se dispara.
- El mensaje se copia al iniciar el temporizador en un
MessageBuffer(búferes de mensajes), un bloque de heap propio del temporizador, y se vuelve a copiar en el heap del receptor cuando se dispara, igual que copia un envío: cualquier término, incluidos los funs locales, viaja sin cambios. - Un receptor que se está ejecutando en un hilo de trabajo recibe el mensaje cuando termina su porción. Un nombre registrado se resuelve cuando el temporizador se dispara; un nombre que nadie tiene, o el pid de un proceso terminado, descarta el mensaje. Los temporizadores enviados a un pid se liberan cuando ese proceso termina.
- El tiempo es en milisegundos relativos en 0..2^49-1 o, con
{abs, true}, unerlang:monotonic_time(millisecond)absoluto; cualquier otra cosa esbadarg. Un plazo más allá del alcance de la rueda se vuelve a armar cuando llega esa ranura. cancel_timer/1,2yread_timer/1,2devuelven los milisegundos restantes ofalse;{async, true}devuelveoky envía{cancel_timer, Ref, Left}({read_timer, ...}),{info, false}devuelveoksin resultado.
Introspección
process_info/1,2 (builtins/erlang/introspection.cpp,
scheduler/introspection.cpp) lee otro proceso bajo el bloqueo del ejecutor y
copia sus términos (mensajes, diccionario) en el heap del llamante; un proceso
que se ejecuta en otro hilo de trabajo se lee cuando termina su porción. El pid de un
proceso que no está vivo da undefined.
- Elementos:
registered_name([]a secas cuando no está registrado),status(running,runnable,waiting),message_queue_len,messages,links,monitors,monitored_by,dictionary,trap_exit,group_leader,heap_size,total_heap_size,stack_size,memory(palabras del heap, sus fragmentos y los binaries fuera del heap, y la pila),reductions(0),priority(normal),error_handler,garbage_collection.process_info/1los enumera desdestatushastagarbage_collection. - Los elementos de OTP que Clause no mantiene (
current_function,initial_call,current_stacktrace, ...) lanzanerror:{notsup, Item}; los elementos desconocidos sonbadarg. processes/0enumera los pids vivos en orden de creación. Un proceso lanzado hereda el group leader de su padre; el primer proceso es su propio group leader.group_leader/2del pid de un proceso que no está vivo esbadarg.garbage_collect/0,1,2solicita una recolección en el siguiente safepoint del destino y devuelvetrue(falsesi no hay proceso vivo); con{async, RequestId}devuelveasyncy envía{garbage_collect, RequestId, Result}.yield/0termina la porción de tiempo en la siguiente entrada de función.
Opciones de spawn
Fuentes: builtins/erlang/spawning.cpp, scheduler/aliases.cpp.
spawn_opt/2..5aceptanlink,monitor,{monitor, MonitorOptions}y las opciones de ajuste{priority, P},{fullsweep_after, N},{min_heap_size, N},{min_bin_vheap_size, N},{max_heap_size, S},{message_queue_data, M},{async_dist, B};{min_heap_size, N}fija el heap mínimo del hijo, y las demás opciones de ajuste se comprueban y se aceptan, pero no se aplican. Un nodo distinto denonode@nohostesbadarg.spawn_request/1..5lanza el proceso de inmediato y envía{Tag, ReqId, ok, Pid}({Tag, ReqId, error, Reason}si falla,badoptpara una opción inválida) según piden{reply_tag, Tag}y{reply, yes | no | error_only | success_only}; conmonitor, el identificador de la solicitud es la referencia del monitor.spawn_request_abandon/1devuelvefalse: la respuesta ya se ha enviado.hibernate(M, F, A)espera hasta que el buzón contiene un mensaje y después llama aM:F(A...)en lugar de la llamada; la pila se conserva, así que el resultado vuelve al llamante.
Alias
Un alias es una referencia a la que se pueden enviar mensajes (Alias ! Msg):
alias/0,1 (explicit_unalias, o reply para un único mensaje), o un monitor
creado con {alias, explicit_unalias | demonitor | reply_demonitor} de
monitor/3 (demonitor termina con el monitor, reply_demonitor además
termina el monitor con su primer mensaje). unalias/1 termina un alias del
llamante. Un envío a una referencia que no es un alias activo, o cuyo
propietario ha terminado, se descarta. Un alias de respuesta solo se consume
cuando se entrega su mensaje, tras cualquier espera por un propietario ocupado.
{tag, Tag} de monitor/3 sustituye 'DOWN' en el mensaje del monitor; los
monitores de puertos no aceptan opciones.
Nodos
Un programa Clause es un único nodo no distribuido, nonode@nohost, y responde
como un nodo de OTP de ese tipo (builtins/erlang/node.cpp): nodes/0 es
[], nodes(this | known) es [nonode@nohost], los demás tipos [];
nodes/2 da {nonode@nohost, #{connection_id => undefined, node_type => this}} para las claves solicitadas. is_alive/0 es false,
monitor_node/2,3 lanzan error:notalive y disconnect_node/1 devuelve
ignored.
Señales de salida
Toda señal de salida procede de un proceso en ejecución (exit/2,
exit_signal/2, link/1) o del final de un proceso. El ejecutor actúa sobre
ella de inmediato cuando su destino no se ejecuta en otro hilo de trabajo, y si
no, en cuanto termina la porción del destino (hilos de trabajo,
scheduler/signals): un destino terminado sale de la cola de ejecución o de su
espera y se finaliza (señales a sus enlaces, su informe de error escrito, su
contexto liberado) antes de que retorne el builtin emisor. Una cadena larga de
enlaces termina proceso a proceso sin recursión.
| Señal en un proceso | Sin atrapar salidas | Atrapando salidas |
|---|---|---|
exit(Pid, kill), exit_signal(Pid, kill) | Termina con la razón killed | Termina con la razón killed |
Razón normal (de un enlace, o enviada a otro proceso) | Nada | Mensaje {'EXIT', From, normal} |
exit(self(), normal) | Termina con la razón normal (peculiaridad de OTP) | Mensaje {'EXIT', Self, normal} |
exit_signal(self(), normal) | Nada | Mensaje {'EXIT', Self, normal} |
Cualquier otra razón, incluido kill de un enlace | Termina con esa razón | Mensaje {'EXIT', From, Reason} |
process_flag(trap_exit, Bool)activa o desactiva la captura y devuelve el valor anterior (inicialmentefalse);process_flag(min_heap_size, N)fija el heap mínimo a partir de la siguiente recolección y devuelve el anterior.- Un proceso terminado por una señal que se envió a sí mismo
(
exit(self(), kill)) se desenrolla atravesando todocatch, todo manejador detryy todo cuerpo deafter: la señal no es una excepción (CallError::exiteden el canal de fallos). exit/2yexit_signal/2devuelventrue; dirigidas a un proceso que ha terminado no hacen nada. Un destino que es una referencia tampoco hace nada, aunque sea un alias activo.- Una señal de salida que termina el proceso principal termina el programa
como un
exitno capturado (estado de salida): la razónnormalsale con 0. - Cuando el heap del receptor rechaza una razón de salida o un mensaje
'EXIT', el receptor falla con un fallo del runtime (salida 70).
Puertos
Los puertos se especifican en puertos: un puerto está enlazado con quien lo abre, participa en enlaces, monitores, señales de salida y nombres registrados como un proceso, y se comunica con su proceso conectado mediante mensajes.
Suspensión de procesos
erlang:suspend_process/1,2 y erlang:resume_process/1
(scheduler/suspension.cpp): cada suspensión es una retención del destino en
el ejecutor, así que un proceso suspendido solo vuelve a ejecutarse cuando se
reanudan todas las suspensiones de todos los suspensores; los mensajes y las
señales siguen llegando (una señal de salida sigue terminándolo), y
process_info(Pid, status) informa de suspended. El final de un suspensor
reanuda sus suspensiones. Una suspensión síncrona de un proceso que se ejecuta
en otro hilo de trabajo espera a que termine su porción de tiempo;
asynchronous retorna de inmediato, {asynchronous, Tag} también envía al
llamante {Tag, suspended}, y unless_suspending responde false cuando el
llamante ya suspende el proceso. Suspender o reanudar al propio llamante, un
proceso terminado, un valor que no sea un pid o unas opciones incorrectas son
badarg, al igual que reanudar un proceso que el llamante no suspende.
Builtins
| Builtin | Comportamiento |
|---|---|
spawn(Fun) | badarg salvo que Fun sea un fun; si lo es, un proceso nuevo llama a Fun(), lanzando {badarity, {Fun, []}} en ese proceso si la aridad es otra |
spawn(M, F, Args) | badarg salvo que M y F sean átomos y Args una lista propia; si no, un proceso nuevo llama a M:F(Args...), lanzando undef en ese proceso cuando ningún módulo la exporta y ningún builtin tiene ese nombre |
spawn_link(Fun), spawn_link(M, F, Args) | Como spawn, y el proceso nuevo queda enlazado con el llamante (enlaces) |
is_process_alive(Pid) | badarg salvo que Pid sea un pid; true mientras su proceso no haya terminado |
spawn_monitor(Fun), spawn_monitor(M, F, Args) | Como spawn, devolviendo {Pid, Ref} de un nuevo monitor |
link(Pid), unlink(Pid) | badarg salvo que Pid sea un pid (enlaces) |
monitor(process, Item) | badarg para otro tipo u otro elemento; una referencia (monitores); Item es un pid o un nombre registrado |
register(Name, Pid), unregister(Name), whereis(Name), registered() | Véase nombres registrados; Name debe ser un átomo |
demonitor(Ref), demonitor(Ref, Options) | badarg salvo que Ref sea una referencia y Options una lista propia de flush e info |
exit(Dest, Reason), exit_signal(Dest, Reason) | badarg salvo que Dest sea un pid o una referencia; true tras la señal de salida |
process_flag(trap_exit, Bool), process_flag(min_heap_size, N), process_flag(save_calls, N) | El valor anterior; badarg para otro indicador, un valor no booleano o un tamaño negativo o no entero |
erlang:send(Dest, Msg), Dest ! Msg | Msg, después de enviarlo (mensajes); send/2 no se importa automáticamente |
spawn(Node, ...), spawn_link(Node, ...), spawn_monitor(Node, ...) (/2, /4) | Este nodo (nonode@nohost): como sin Node. Otro nodo: el pid inútil de OTP de un nodo que no está activo, un proceso que termina de inmediato con la razón noconnection, así que un enlace o monitor informa de noconnection; badarg para un nodo que no sea un átomo |
erlang:send(Dest, Msg, Options) | ok, o noconnect para un mensaje a otro nodo con la opción noconnect; opciones nosuspend, noconnect, priority (un mensaje ordinario) |
send_nosuspend(Dest, Msg), send_nosuspend(Dest, Msg, [noconnect]) | true; false para otro nodo con noconnect |
link(Pid, Options), exit(Dest, Reason, Options), exit_signal(Dest, Reason, Options) | Como sin opciones; Options es [] o [priority] (una señal ordinaria) |
process_flag(Pid, save_calls, N) | El valor anterior de otro proceso (0..10000); se conserva, pero Clause no guarda llamadas; otros indicadores dan badarg |
processes_iterator(), processes_next(Iter) | Recorren los procesos vivos: {Pid, Iter1} o none; un proceso lanzado durante el recorrido puede aparecer |
bump_reductions(N) | true para un entero no negativo; Clause no cuenta reducciones |
erlang:suspend_process(Pid), erlang:suspend_process(Pid, Options), erlang:resume_process(Pid) | Véase suspensión de procesos |
erlang:process_display(Pid, backtrace) | true tras escribir la pila de llamadas del proceso vivo Pid en la salida de error estándar, un Module:Function/Arity por línea, la función actual primero; badarg para otro tipo o un proceso terminado |
El proceso nuevo se añade al final de la cola del hilo de trabajo de su padre
(hilos de trabajo); spawn devuelve su pid de inmediato.
Clause