Процеси
План 11, крок 43 (2026-10-08): запущені процеси на кооперативному виконавці (executor); крок 44: причини виходу й звіти про помилки; крок 45: надсилання повідомлень; крок 46: вибіркове отримання (selective receive); крок 47: тайм-аути receive; крок 48: зв'язки (links) і сигнали виходу; крок 49: монітори; крок 50: зареєстровані імена; крок 53: порти (жодних); крок 56: робочі потоки планувальника; крок 57: пробудження між робочими потоками й завершення роботи.
Виконавець
Один runtime виконує свої процеси на своїх робочих потоках планувальника (робочі потоки, модель виконання). Під час запуску виклик вхідної функції стає першим процесом, головним процесом, і виконавець працює, доки той не завершиться:
- Готові до виконання процеси чекають в одній черзі «перший прийшов —
перший вийшов». Робочий потік виконує перший із них протягом кванту часу в
4 000 редукцій (
CONTEXT_REDSв OTP), потім повертає його в кінець черги, якщо той не завершився. - Кожен вхід у функцію витрачає одну редукцію: виклики, хвостові виклики, виклики fun і динамічні виклики, вбудовані функції. Коли редукцій не лишилося, вхід не відбувається: процес запам'ятовує функцію, в яку входив, тримає її аргументи у своїх регістрах (вони лишаються коренями збирання) і повертається до виконавця, який згодом відновлює його, повторюючи вхід. Цикл, що не робить викликів (comprehension над списком без викликів у тілі), виконується до кінця, перш ніж процес зможе поступитися (yield). Вбудовані функції, робота яких зростає зі списком чи binary в аргументі, виконуються порціями й поступаються між ними (порції).
- Кожен процес володіє своєю купою й стеком. Аргументи нового процесу копіюються в його купу (копіювання між купами).
- Процес завершується, коли його перший виклик повертає значення або
породжує виняток. Контекст, купа й стек завершеного процесу звільняються
одразу; його pid лишається дійсним термом, і
is_process_alive/1повертає для нього false. - Процес, що чекає в
receive, не перебуває в черзі; надсилання йому повертає його в кінець черги (receive). - Коли головний процес завершується, програма завершується з його
результатом (виконувані файли): процеси, що ще стоять у
черзі чи чекають, звільняються без подальшого виконання, як escript в OTP
зупиняється, коли
main/1повертає значення. - Сигнал виходу, що завершує головний процес, завершує програму (сигнали виходу).
erlang:halt/0,1у будь-якому процесі завершує програму з його статусом. Збій runtime у будь-якому процесі (вичерпано пам'ять, перевищено необов'язкове обмеження, внутрішня помилка) завершує програму як збій runtime (код виходу 70). Виняток Erlang завершує лише процес, що його породив (виходи).- Виклики експортованих функцій з боку хоста (
CLAUSE_invoke_v1) виконують свою функцію в контексті, що викликає, до завершення, відновлюючи її після кожного поступання без виконання інших процесів; програми, запущені черезCLAUSE_main_v1, використовують виконавця.
Робочі потоки
Крок плану 56 (2026-10-08). Виконавець виконує процеси на
RuntimeOptions::schedulers робочих потоках: потоці, що запустив програму,
і ще одному потоці на кожен додатковий робочий потік. Програми беруть
кількість із --schedulers N (від 1 до 1 024,
опції runtime); типово — один робочий
потік на логічний процесор, як +S в OTP.
- Кожен робочий потік бере перший процес зі спільної черги (задокументована альтернатива чергам для кожного робочого потоку з перехопленням роботи (work stealing): одна черга зберігає порядок OTP «перший прийшов — перший вийшов» і не потребує перехоплення). Вільні робочі потоки сплять, доки процес не стане в чергу або не спливе найраніший тайм-аут receive.
- Один м'ютекс виконавця захищає чергу, процеси, що чекають, таймери, зареєстровані імена, зв'язки й монітори кожного процесу та кожен процес, що не виконується. Купа, стек, поштова скринька й канал збоїв процесу, що виконується, належать лише його робочому потоку; код Erlang виконується без блокування.
- Зв'язки, монітори й імена змінюються під блокуванням одразу, також для
процесу, що виконується деінде. Надсилання або сигнал виходу від
exit/2чиexit_signal/2процесу, що виконується на іншому робочому потоці, не може торкатися його купи: вбудована функція нічого не робить, а її процес завершує свій квант (builtins::Blocked); він чекає, доки квант цілі не закінчиться, потім виконується першим і повторює вбудовану функцію з тими самими аргументами. Ціль утримується до того моменту, тож повторена вбудована функція не може знову застати її під час виконання. Процес, що завершується зі зв'язками чи моніторами, чиї процеси виконуються деінде, так само доводиться до завершення, щойно їхні кванти закінчуються. - Отже, повідомлення й сигнали виходу й надалі набувають чинності в момент
надсилання: надсилання повертає керування після того, як повідомлення
опинилося в поштовій скриньці отримувача, а
is_process_alive/1післяexit(Pid, kill)дає false, як OTP обіцяє для сигналів від того, хто викликає. - Запуск (spawn) зв'язує новий процес або встановлює на нього монітор
(
spawn_link,spawn_monitor) до того, як будь-який робочий потік зможе його виконати. Запущений процес може виконатися раніше, ніж продовжить батьківський, тож програма, що встановлює монітор чи зв'язок із короткоживучим процесом післяspawn/1, може побачитиnoproc, як в OTP з кількома планувальниками. - Завершення головного процесу, halt або збій runtime зупиняють програму, щойно кожен робочий потік закінчив свій поточний квант; після цього інші процеси звільняються.
- Спільні служби runtime синхронізовано (потоки): атоми, сервер коду, номери pid і облік пам'яті.
- Вивід різних процесів перемежовується в порядку, в якому відбуваються
їхні записи. Кожен виклик
io:formatіerlang:displayпише свій текст одразу.
Пробудження й завершення роботи (крок плану 57) не потребують додаткового механізму, бо кожна зміна стану планування процесу відбувається під м'ютексом виконавця:
- Повідомлення,
'EXIT'чи'DOWN'для процесу, що чекає, ставить його в чергу в тій самій критичній секції, що й доставляє, і будить один вільний робочий потік. Процес, що ще перебуває в кванті, в якому почав чекати, не може тоді отримати повідомлення (його відправники чекають кінця кванту), тож доставка завжди застає його припаркованим. - Робочий потік, що не знаходить готового процесу, спить, доки інший робочий потік не поставить процес у чергу, не настане найраніший тайм-аут receive чи кінець програми; паркування процесу з тайм-аутом будить усі вільні робочі потоки, щоб вони чекали нового терміну. Зайняті робочі потоки перевіряють таймери перед кожним квантом.
- Повідомлення й тайм-аут, що спливає, одного receive можуть змагатися: той, хто прийде першим, ставить процес у чергу й скасовує іншого, а receive бере повідомлення, що надійшло до його відновлення, як і в OTP.
- Сигнал виходу процесу, що чекає, стоїть у черзі, утримується чи заблокований, забирає його звідти, де він є, перш ніж його буде завершено.
- Коли програма завершується, кожен робочий потік закінчує свій поточний
квант і зупиняється;
run()повертає керування після їх приєднання, і кожен інший процес, запущений виконавцем, звільняється, тож runtime завершує роботу без жодного контексту, що лишився. - Еталон OTP
executables_wakeupsнавантажує це з 1, 2, 4 і всіма робочими потоками: отримувачі, чиї тайм-аути 0–2 мс змагаються з повідомленнями відправників, ланцюг із 16 зв'язаних процесів, що крутяться в циклі, завершений одним сигналом виходу, з монітором на кожному учасникові, процеси під моніторами, що завершуються, коли прокидається їхній спостерігач, програма, що завершується, поки процеси крутяться, чекають і засипають один одного повідомленнями, і halt в іншому процесі.
Виходи
Процес, відмінний від головного, завершується з причиною виходу, як в OTP
(detail::exit_reason, process/exits). Сигнали виходу передають її
зв'язаним процесам (зв'язки), а повідомлення 'DOWN' —
процесам-моніторам (монітори); звіти про помилки показують її.
| Як завершується процес | Причина виходу | Звіт про помилку |
|---|---|---|
| Його перший виклик повертає значення | normal | Немає |
exit(Reason) (також normal, kill) | Reason | Немає |
Помилка (error/1,2,3, badarith, {badmatch, V}, undef, ...) | {Reason, Stack} | Так |
Неперехоплений throw(Value) | {{nocatch, Value}, Stack} | Так |
| Його завершує сигнал виходу (сигнали виходу) | Причина сигналу, killed для exit(Pid, kill) | Немає |
Звіт про помилку пишеться в stderr, коли процес завершується, після скидання стандартного виводу, у форматі типового обробника журналу (logger) 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,[]}]}
Заголовок містить місцевий час; причина розкладається так, як це робить
~p. Головний процес такого звіту не пише: його неперехоплений виняток є
винятком програми (виконувані файли).
Повідомлення
Dest ! Msg і erlang:send(Dest, Msg) (крок плану 45) спершу обчислюють
Dest і повертають Msg.
Dest | Ефект |
|---|---|
| pid живого процесу | Msg копіюється в купу отримувача (копіювання між купами) зі збереженням спільних частин і додається до його вхідної черги сигналів |
| pid процесу, що завершився | Нічого; надсилання успішне |
| Зареєстроване ім'я | Як для його pid; badarg, коли жоден живий процес не має цього імені |
{Name, nonode@nohost} із двох атомів | Як для pid, зареєстрованого як Name; нічого, коли такого немає |
{Name, Node} для будь-якого іншого вузла | Нічого (інших вузлів немає) |
| Будь-що інше | badarg |
- Повідомлення одного відправника надходять у тому порядку, в якому він їх надіслав; надсилання самому собі — таке саме повідомлення, як будь-яке інше.
- Кожне повідомлення є коренем збирання свого отримувача, доки receive його не забере (купа runtime).
- Поштова скринька отримувача (
Mailbox) містить вхідну чергу сигналів, куди додають надсилання, і чергу повідомлень, яку receive переглядає зі збереженої позиції: повідомлення, що надійшли, переміщуються в кінець черги, коли receive їх розглядає. - Коли купа отримувача відмовляє в копіюванні (необов'язкове обмеження на
кшталт
--max-heapабо нестача пам'яті в хоста), нічого не доставляється, а процес-відправник зазнає збою runtime (код виходу 70), як будь-який процес, що перевищує обмеження.
Receive
receive (кроки плану 46–47) обирає серед своїх клауз, як case, над
повідомленнями поштової скриньки:
- Перегляд поштової скриньки починається з найстарішого повідомлення. Кожне
повідомлення по черзі зіставляється з клаузами (зразки й guard, які можуть
читати зв'язування, зроблені до
receive); перше повідомлення, з яким зіставляється якась клауза, вилучається, і виконується тіло цієї клаузи. Повідомлення, з якими не зіставляється жодна клауза, лишаються в поштовій скриньці у своєму порядку; наступний receive знову починає з найстарішого повідомлення. - Коли кожне повідомлення розглянуто, процес чекає (
CLAUSE_wait_frame_v1, вбудована функція, в яку входять як у виклик): він залишає чергу виконання, доки надсилання не доставить йому повідомлення, а тоді перегляд продовжується з повідомленнями, що надійшли. Процеси, що чекають, зберігають свої фрейми й повідомлення як корені збирання і збирають сміття після відновлення (процеси, що чекають). - Імена, зв'язані в кожній клаузі, а також у тілі
after, якщо воно є, експортуються післяreceive, як дляcase; виклик у хвостовій позиції тіла клаузи чиafterє хвостовим викликом, тож цикл сервера виконується в сталому обсязі стека. after T -> Body:Tобчислюється першим, до перегляду. Коли receive мав би чекати,Tмає бутиinfinityабо цілим числом у 0..4294967295 (мілісекунди), інакшеerror:timeout_value; повідомлення, що зіставляється одразу, ніколи його не перевіряє, як в OTP.after 0виконуєBody, щойно кожне повідомлення розглянуто. Скінченний тайм-аут починається, коли receive уперше чекає, і не перезапускається повідомленнями, що не зіставляються з жодною клаузою; коли він спливає,Bodyвиконується зі зв'язуваннями до receive, а наступний receive переглядає з найстарішого повідомлення. Повідомлення, що надійшло до спливання тайм-ауту, забирається. Receive лише зafter— це сон (ідіомаtimer:sleep/1).- Згенерований код: голова циклу зазирає до наступного нерозглянутого
повідомлення (
CLAUSE_receive_v1peek, у кореневий слот), вибір clause забирає зіставлене повідомлення (take) перед її тілом або пропускає незіставлене (skip) і повторює цикл; коли повідомлень не лишилося, цикл входить в очікування з тайм-аутом, яке відповідаєtrue(переглянути знову) абоfalse(тайм-аут:restart, потім тілоafter). - Виконавець тримає таймер для кожного процесу, що чекає зі скінченним тайм-аутом: таймер, що сплив, повертає процес у чергу, а коли жоден процес не може виконуватися, виконавець спить до найранішого таймера. Тайм-аути вимірюються за монотонним годинником у мілісекундах і ніколи не спрацьовують раніше.
- Коли кожен процес чекає без тайм-ауту на повідомлення, яке ніхто не може
надіслати, програма чекає вічно, як і в OTP. Виклик із боку хоста
(
CLAUSE_invoke_v1), який мав би чекати, натомість завершується зbusy: під час нього жоден інший процес не виконується.
Зв'язки
Крок плану 48. Зв'язок з'єднує два процеси в обох напрямках (Signals у
кожному контексті: зв'язані pid у порядку створення зв'язків і прапорець
trap_exit).
link(Pid)зв'язує того, хто викликає, з живим процесом і повертаєtrue; зв'язування із самим собою чи повторне зв'язування нічого не робить. Для процесу, що завершився, породжуєerror:noproc, або, коли той, хто викликає, перехоплює виходи, повертаєtrueі надсилає йому{'EXIT', Pid, noproc}, як це робить локальнийlink/1в OTP.unlink(Pid)видаляє зв'язок з обох боків; після повернення зв'язок не має ефекту.trueтакож тоді, коли зв'язку не було.spawn_link/1,3зв'язують новий процес із тим, хто викликає, до його виконання.- Коли процес завершується, кожен зв'язаний процес отримує сигнал виходу з його причиною виходу (виходи), і зв'язок зникає. Зв'язаним процесам сигнали надсилаються в порядку створення зв'язків.
Монітори
Крок плану 49. Монітор односпрямований: процес-монітор тримає його (за
посиланням, у Signals), а процес під монітором зберігає посилання й pid
монітора, щоб надіслати повідомлення, коли завершиться.
monitor(process, Pid)повертає нове посилання. Коли процес завершується, той, хто викликав, отримує{'DOWN', Ref, process, Pid, Reason}з його причиною виходу (виходи); для процесу, що вже завершився, повідомлення надходить одразу з причиноюnoproc. Монітор на самого себе нічого не створює. Кожен виклик створює окремий монітор зі своїм повідомленням; повідомлення одного процесу надсилаються в порядку створення моніторів.demonitor(Ref)зупиняє монітор: жодне його'DOWN'після цього не надходить. Повертаєtrue, також для посилання, що не є активним монітором того, хто викликає.demonitor(Ref, Options):infoповертає, чи монітор був ще активним;flushвидаляє найстаріше повідомлення{_, Ref, _, _, _}, якщо він не був активним (його'DOWN'уже в поштовій скриньці), як це робить OTP.spawn_monitor/1,3повертають{Pid, Ref}— монітор, створений до виконання нового процесу.- Процес, що завершується, відкидає монітори, які тримає.
monitor(process, Name)іmonitor(process, {Name, nonode@nohost})стежать за процесом, зареєстрованим якName(одразуnoproc, коли такого немає); його'DOWN'називає{Name, nonode@nohost}замість pid.{Name, Node}для іншого вузла даєbadarg.monitor(port, Port | Name)стежить за портом (порти):{'DOWN', Ref, port, Port, Reason}, коли він закривається. Pid дляportабо порт дляprocessдаєbadarg.
Зареєстровані імена
Крок плану 50. Виконавець тримає одну таблицю імен (атомів) до pid, а кожен
процес — власне ім'я (Signals::name).
register(Name, Pid)дає ім'я живому процесу:badargдляundefined, імені, що вже використовується, процесу, що вже має ім'я, процесу, що завершився, абоPid, що не є pid. Процес може зареєструвати сам себе.unregister(Name)звільняє ім'я (badarg, коли жоден процес його не має);whereis(Name)— це pid абоundefined;registered()перелічує імена в порядку таблиці атомів (порядок OTP теж не визначено).- Ім'я процесу звільняється, коли він завершується, до надсилання сигналів
його зв'язкам і моніторам, тож отримувач
'DOWN'чи'EXIT'може знову зареєструвати це ім'я, як в OTP.
Сигнали виходу
Кожен сигнал виходу надходить від процесу, що виконується (exit/2,
exit_signal/2, link/1), або від завершення процесу. Виконавець обробляє
його одразу, коли його ціль не виконується на іншому робочому потоці, інакше
— щойно квант цілі закінчився (робочі потоки,
scheduler/signals): завершена ціль залишає чергу виконання чи своє
очікування і доводиться до завершення (сигнали її зв'язкам надіслано, звіт
про помилку записано, контекст звільнено) до того, як вбудована функція
надсилання поверне керування. Довгий зв'язаний ланцюг завершується процес за
процесом без рекурсії.
| Сигнал процесу | Без перехоплення виходів | З перехопленням виходів |
|---|---|---|
exit(Pid, kill), exit_signal(Pid, kill) | Завершується з причиною killed | Завершується з причиною killed |
Причина normal (від зв'язку або надіслана іншому процесу) | Нічого | Повідомлення {'EXIT', From, normal} |
exit(self(), normal) | Завершується з причиною normal (особливість OTP) | Повідомлення {'EXIT', Self, normal} |
exit_signal(self(), normal) | Нічого | Повідомлення {'EXIT', Self, normal} |
Будь-яка інша причина, включно з kill від зв'язку | Завершується з цією причиною | Повідомлення {'EXIT', From, Reason} |
process_flag(trap_exit, Bool)вмикає чи вимикає перехоплення й повертає попереднє значення (початковоfalse).- Процес, завершений сигналом, який він надіслав сам собі
(
exit(self(), kill)), розмотується повз коженcatch, обробникtryі тілоafter: сигнал не є винятком (CallError::exitedу каналі збоїв). exit/2іexit_signal/2повертаютьtrue; для процесу, що завершився, вони нічого не роблять. Посилання як адресат теж нічого не робить (псевдонімів процесів немає).- Сигнал виходу, що завершує головний процес, завершує програму як
неперехоплений
exit(статус виходу): причинаnormalдає код виходу 0. - Коли купа отримувача відмовляє в причині виходу чи повідомленні
'EXIT', отримувач зазнає збою runtime (код виходу 70).
Порти
Порти (кроки плану 57A–57F) описано в портах: порт зв'язаний із
процесом, що його відкрив, бере участь у зв'язках, моніторах, сигналах
виходу й зареєстрованих іменах, як процес, і спілкується зі своїм
під'єднаним процесом повідомленнями. Крок 57B надає ідентичності, таблицю
портів, вбудовані функції портів і порти {fd, In, Out} лише для виводу.
Вбудовані функції
| Вбудована функція | Поведінка |
|---|---|
spawn(Fun) | badarg, якщо Fun не fun; інакше новий процес викликає Fun(), породжуючи в цьому процесі {badarity, {Fun, []}} для іншої арності |
spawn(M, F, Args) | badarg, якщо M і F не атоми або Args не правильний список; інакше новий процес викликає M:F(Args...), породжуючи в цьому процесі undef, коли жоден модуль її не експортує і немає вбудованої функції з таким ім'ям |
spawn_link(Fun), spawn_link(M, F, Args) | Як spawn, і новий процес зв'язується з тим, хто викликає (зв'язки) |
is_process_alive(Pid) | badarg, якщо Pid не pid; true, доки його процес не завершився |
spawn_monitor(Fun), spawn_monitor(M, F, Args) | Як spawn, повертаючи {Pid, Ref} нового монітора |
link(Pid), unlink(Pid) | badarg, якщо Pid не pid (зв'язки) |
monitor(process, Item) | badarg для іншого типу чи елемента; посилання (монітори); Item — це pid або зареєстроване ім'я |
register(Name, Pid), unregister(Name), whereis(Name), registered() | Див. зареєстровані імена; Name має бути атомом |
demonitor(Ref), demonitor(Ref, Options) | badarg, якщо Ref не посилання або Options не правильний список із flush та info |
exit(Dest, Reason), exit_signal(Dest, Reason) | badarg, якщо Dest не pid і не посилання; true після сигналу виходу |
process_flag(trap_exit, Bool) | Попереднє значення; badarg для іншого прапорця чи не булевого значення |
erlang:send(Dest, Msg), Dest ! Msg | Msg після його надсилання (повідомлення); send/2 не імпортується автоматично |
Новий процес стає в чергу за всіма готовими до виконання процесами; spawn
одразу повертає його pid.
Clause