Процеси
Виконавець
Один runtime виконує свої процеси на своїх робочих потоках планувальника (робочі потоки, модель виконання). Під час запуску виклик вхідної функції стає першим процесом, головним процесом, і виконавець (executor) працює, доки той не завершиться:
- Готові до виконання процеси чекають у черзі виконання свого робочого
потоку за принципом «перший прийшов — перший вийшов»
(робочі потоки). Робочий потік виконує перший із них протягом
кванту часу в 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, використовують виконавця.
Робочі потоки
Виконавець виконує процеси на RuntimeOptions::schedulers робочих потоках:
потоці, що запустив програму, і ще одному потоці на кожен додатковий робочий
потік. Програми беруть кількість із --schedulers N (від 1 до 1 024,
параметри runtime); типово — один робочий
потік на логічний процесор, на якому програма може виконуватися (її маска
спорідненості, як активні планувальники OTP), або на кожен CPU із
--scheduler-cpus.
- Кожен робочий потік має власну чергу виконання, як кожен планувальник ERTS: процес лишається з робочим потоком, на якому виконувався востаннє, тож його купа лишається в кешах цього ядра. Запущений процес починає на робочому потоці свого батька. Вільні робочі потоки сплять, доки для них не стане в чергу процес або не з'явиться робота, яку можна перехопити; потік таймерів ставить у чергу процеси, чий тайм-аут receive сплив (тайм-аути receive).
- Процеси мігрують, як в ERTS, який періодично балансує свої черги виконання: кожні 4 кванти робочий потік порівнює свою чергу з чергою сусіда і забирає з неї один процес, коли сусід має щонайменше на два більше. Робочий потік із порожньою чергою перехоплює (work stealing) один процес із найдовшої черги, що має щонайменше два, або єдиний процес сплячого робочого потоку, який виконує, перш ніж той прокинеться.
- Пробудження ліниві, як ERTS тримає процеси, що спілкуються між собою, на одному планувальнику: процес, що виконується і будить процес сплячого робочого потоку з порожньою чергою, ставить його в чергу свого робочого потоку, який забирає його, коли закінчується квант; інший робочий потік будиться лише тоді, коли черга після цього містить більше одного процесу. Потоки таймерів і вводу-виводу, завдання портів і робочі потоки між квантами ставлять процес у чергу його власного робочого потоку або вільного робочого потоку, коли власний зайнятий.
- Один м'ютекс виконавця захищає процеси, що чекають, таймери, зареєстровані імена, зв'язки й монітори кожного процесу та кожен процес, що не виконується. Купа, стек, поштова скринька й канал збоїв процесу, що виконується, належать лише його робочому потоку; код Erlang виконується без блокування. Черги виконання й стан простою робочих потоків мають власне блокування, яке береться після м'ютекса виконавця; вільні робочі потоки сплять на ньому (створення й знищення процесів).
- Запуск будує контекст нового процесу й копіює в нього виклик без жодного
блокування. Запуск без зв'язку чи монітора потім публікує його в таблиці
процесів і ставить у чергу лише під їхніми власними блокуваннями, ніколи не
беручи м'ютекс виконавця;
spawn_link,spawn_monitorі формиspawn_opt/spawn_requestз монітором публікують, зв'язують чи встановлюють монітор і ставлять у чергу під м'ютексом виконавця. Робочий потік звільняє контексти завершених процесів одразу після виходу з-під м'ютекса перед наступним квантом. - Процес потребує шести виділень пам'яті за своє життя (вартість створення процесу): його контекст, прив'язки контексту до runtime зі станом планування, сховище купи й перший блок купи, надходження та токен часу життя. Перші 64 слова стека й чотири вузли списку поштової скриньки живуть усередині контексту, map моніторів створюються першим монітором, а реєстри runtime (контексти, звільнення персистентних термів) не тримають окремого вузла на процес.
- Зв'язки, монітори й імена змінюються під блокуванням одразу, також для
процесу, що виконується деінде. Повідомлення чекає в буфері серед
надходжень отримувача, доки той не забере його
(буфери надходжень). Сигнал виходу від
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пише свій текст одразу.
Пробудження й завершення роботи не потребують додаткового механізму, бо кожна зміна стану планування процесу відбувається під м'ютексом виконавця:
- Повідомлення,
'EXIT'чи'DOWN'для процесу, що чекає, ставить його в чергу в тій самій критичній секції, що й доставляє, і будить робочий потік, як описано вище. Процес, що ще перебуває в кванті, в якому почав чекати, не може тоді отримати повідомлення (його відправники чекають кінця кванту), тож доставка завжди застає його припаркованим. Опубліковане повідомлення (буфери надходжень) теж не губиться: паркування перевіряє надходження під їхнім блокуванням, а публікація, що застає отримувача припаркованим, будить його під м'ютексом виконавця. - Робочий потік, що не знаходить готового процесу, спить, доки інший робочий потік, надсилання, запуск процесу чи потік таймерів не поставить процес у чергу або програма не завершиться; він перевіряє черги й засинає під блокуванням черг виконання, яке береться під час кожного додавання в чергу. Робочі потоки ніколи не читають годинник для таймерів; паркування процесу з тайм-аутом, ранішим за всі інші, будить лише потік таймерів.
- Повідомлення й тайм-аут, що спливає, одного receive можуть змагатися: той, хто прийде першим, ставить процес у чергу й скасовує іншого, а receive бере повідомлення, що надійшло до його відновлення, як і в OTP.
- Сигнал виходу процесу, що чекає, стоїть у черзі, утримується чи заблокований, забирає його звідти, де він є, перш ніж його буде завершено.
- Коли програма завершується, кожен робочий потік закінчує свій поточний
квант і зупиняється;
run()повертає керування після їх приєднання, і кожен інший процес, запущений виконавцем, звільняється, тож runtime завершує роботу без жодного контексту, що лишився. - Еталон OTP
executables_wakeupsнавантажує це з 1, 2, 4 і всіма робочими потоками: отримувачі, чиї тайм-аути 0–2 мс змагаються з повідомленнями відправників, ланцюг із 16 зв'язаних процесів, що крутяться в циклі, завершений одним сигналом виходу, з монітором на кожному учасникові, процеси під моніторами, що завершуються, коли прокидається їхній спостерігач, програма, що завершується, поки процеси крутяться, чекають і засипають один одного повідомленнями, і halt в іншому процесі.
Прив'язка до CPU
Типово робочі потоки не прив'язані: ОС розміщує їх на будь-якому CPU з маски
спорідненості програми, як типове +sbt u в OTP. --scheduler-cpus SET
(параметри runtime) прив'язує робочий потік
N до N-го CPU набору, циклічно, якщо робочих потоків більше, як
+sbt/+sct в OTP; потоки таймерів і вводу-виводу лишаються неприв'язаними.
performance— найшвидший клас ядер: ядра Windows з найвищимEfficiencyClass, Linux/sys/devices/cpu_core/cpus(гібридні Intel) або CPU з найбільшимcpu_capacity; на процесорі з однаковими ядрами — кожен CPU.all— кожен CPU маски спорідненості. Список (0-15,2,4,6, CPU до 1023) має містити лише CPU з маски спорідненості:--scheduler-cpus: CPU N is not available to the program, код виходу 70.- Windows нумерує CPU групи процесорів процесу (щонайбільше 64);
performanceчитає групу 0. - macOS не має спорідненості потоків: параметр приймається,
performanceвраховує перший рівень продуктивності (hw.perflevel0.logicalcpu) для типової кількості робочих потоків, а прив'язка лише підвищує кожен робочий потік до класу QoSUSER_INTERACTIVE, який macOS обслуговує насамперед на P-ядрах.
Тайм-аути receive
Таймери живуть в ієрархічному хешованому колесі таймерів (timer wheel), як
їх тримає ERTS (scheduler/timer_wheel):
- Тіки — це мілісекунди від запуску виконавця. Шість рівнів по 64 слоти охоплюють 2^36 тіків, більше за найдовший тайм-аут (4,294,967,295 мс). Таймер лежить у слоті найвищої 6-бітної цифри, у якій його тік відрізняється від поточного тіку колеса; зайняті слоти — бітові карти для кожного рівня.
- Взведення (процес, що чекає, паркується) і скасування (повідомлення, сигнал виходу або завершення процесу надходить раніше) від'єднують або приєднують вузол інтрузивного списку, що зберігається разом зі станом планування процесу: за сталий час. Термін стає тіком у ньому або після нього, тож жоден тайм-аут не спрацьовує раніше.
- Один потік таймерів на запуск спить до наступного зайнятого слота (першого зайнятого слота після поточного на будь-якому рівні), один раз читає годинник, коли прокидається, і просуває колесо до цього тіку: слоти вищих рівнів, досягнуті на своєму початку, переносять свої таймери нижче, а слот рівня 0 спрацьовує. Розглядаються лише слоти, чий час настав; процеси, що чекають, ніколи не переглядаються. Процеси з тайм-аутом, що сплив, стають у чергу в порядку тіків.
- Колесо спільне для робочих потоків під м'ютексом виконавця.
after 0ніколи не чекає, аinfinityніколи не взводить таймер. --profileповідомляє кванти часу й зчитування годинника потоком таймерів (профілювання). Таймери спрацьовують лише з такою точністю, як очікування з тайм-аутом хоста (у Windows — типова роздільність таймера).runtime_timer_wheelвзводить 20,000 таймерів на всіх рівнях, скасовує 90% і просуває колесо нерівними кроками: кожен таймер, що лишився, спрацьовує один раз, у першому просуванні, що досягає його тіку, у порядку тіків; жоден скасований не спрацьовує.
Виходи
Процес, відмінний від головного, завершується з причиною виходу, як в 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) спершу обчислюють Dest і
повертають Msg.
Dest | Ефект |
|---|---|
| pid живого процесу | Msg копіюється пласко в купу отримувача (копіювання між купами), як OTP копіює повідомлення, а велике — у блок, який отримувач перебирає собі (передавання повідомлень), і додається до його вхідної черги сигналів; для отримувача на іншому робочому потоці — у буфер серед його надходжень, який він забере під час наступної перевірки (буфери надходжень) |
| pid процесу, що завершився | Нічого; надсилання успішне |
| Зареєстроване ім'я | Як для його pid; badarg, коли жоден живий процес не має цього імені |
{Name, nonode@nohost} із двох атомів | Як для pid, зареєстрованого як Name; нічого, коли такого немає |
{Name, Node} для будь-якого іншого вузла | Нічого (інших вузлів немає) |
| Посилання (reference) | Як для власника активного псевдоніма; інакше нічого |
| Будь-що інше | badarg |
- Повідомлення одного відправника надходять у тому порядку, в якому він їх надіслав; надсилання самому собі — таке саме повідомлення, як будь-яке інше.
- Кожне повідомлення є коренем збирання свого отримувача, доки receive його не забере (купа runtime).
- Поштова скринька отримувача (
Mailbox) містить вхідну чергу сигналів, куди додають надсилання, і чергу повідомлень, яку receive переглядає зі збереженої позиції: повідомлення, що надійшли, переміщуються в кінець черги, коли receive їх розглядає. Обидва списки повторно використовують до 64 запасних вузлів поштової скриньки, тож надсилання й отримання повідомлення не виділяє вузла в купі C++. - Коли купа отримувача відмовляє в копіюванні (необов'язкове обмеження на
кшталт
--max-heapабо нестача пам'яті в хоста), нічого не доставляється, а процес-відправник зазнає збою runtime (код виходу 70), як будь-який процес, що перевищує обмеження; отримувач, що відмовляє в повідомленні зі своїх надходжень, сам зазнає збою.
Буфери надходжень
Надсилання процесу, що виконується на іншому робочому потоці, взагалі не бере блокувань (створення й знищення процесів):
- Таблиця процесів runtime (
ProcessTable) — масив атомарних слотів із відкритою адресацією, який надсилання на робочому потоці читає без жодного блокування: номери pid ніколи не використовуються повторно, тож стертий слот лишається позначкою видалення, доки вставлення не перебудує масив. Вставлення й стирання беруть блокування записувача й також тримають блокування виконавця, під яким пошуки більше нічого не потребують. - Кожен процес має свої надходження (
Arrivals): список без блокувань, у який відправники додають повідомлення, з копією стану виконання процесу у виконавці. Відправник знаходить надходження отримувача й читає цей стан; відправник, чиє останнє надсилання тому самому отримувачу застало його не під час виконання, пропускає пошук. - Відправник обходить повідомлення один раз до будь-якого блокування
(один обхід повідомлення). Для
отримувача, що виконується, він копіює повідомлення відразу за вузлом
списку одним виділенням пам'яті (велике повідомлення або повідомлення з
binary поза купою — у
MessageBuffer) і додає його; отримувач згодом копіює ці слова у свою купу й переміщує їхні вказівники. Будь-який інший отримувач отримує повідомлення під блокуванням виконавця, як раніше: скопійованим у його купу або доданим до його надходжень, якщо він тим часом почав квант чи ще має надходження, тож повідомлення одного відправника ніколи не обганяють одне одного, а відправник ніколи не копіює накопичені повідомлення під блокуванням. Обидва результати гонки коректні. - Отримувач забирає надходження у свою купу й вхідну чергу сигналів,
починаючи з найстарішого: по 64, коли його receive розглянув усі
повідомлення й мав би чекати, тож потік повідомлень досягає купи з тією
швидкістю, з якою receive споживає їх (OTP тримає такі повідомлення у
фрагментах купи, доки вони не знадобляться); усі — коли переходить у
гібернацію і перед доставкою будь-чого іншого під блокуванням виконавця
(повідомлення,
'EXIT','DOWN', повідомлення порту чи псевдоніма), тож повідомлення кожного відправника зберігають порядок відносно його інших сигналів.process_infoзабирає їх перед підрахунком чи переліком повідомлень. Повідомлення таймерів та інші повідомлення процесу, що виконується, теж чекають серед надходжень. - Процес паркується під блокуванням виконавця лише після послідовно узгодженої перевірки, що жодне надходження не чекає; додавання, що застає його припаркованим, бере блокування виконавця й будить його.
- Надходження завершеного процесу й замінені масиви таблиці звільняються лише після того, як кожен робочий потік пройшов межу кванту після їх вилучення або чекає роботи (звільнення в стані спокою): робочий потік записує епоху звільнення на кожній межі кванту, а надсилання тримає знайдене лише в межах свого кванту.
- Надсилання самому собі йде прямо у власну вхідну чергу сигналів відправника.
Receive
receive обирає серед своїх клауз, як case, над повідомленнями поштової
скриньки:
- Перегляд поштової скриньки починається з найстарішого повідомлення. Кожне
повідомлення по черзі зіставляється з клаузами (зразки й guard, які можуть
читати зв'язування, зроблені до
receive); перше повідомлення, з яким зіставляється якась клауза, вилучається, і виконується тіло цієї клаузи. Повідомлення, з якими не зіставляється жодна клауза, лишаються в поштовій скриньці у своєму порядку; наступний receive знову починає з найстарішого повідомлення. - Коли кожне повідомлення розглянуто, процес чекає (
CLAUSE_wait_frame_v1, вбудована функція, в яку входять як у виклик): він залишає чергу виконання, доки надсилання не доставить йому повідомлення, а тоді перегляд продовжується з повідомленнями, що надійшли. Процеси, що чекають, зберігають свої фрейми й повідомлення як корені збирання і збирають сміття після відновлення (процеси, що чекають). - Імена, зв'язані в кожній клаузі, а також у тілі
after, якщо воно є, експортуються післяreceive, як дляcase; виклик у хвостовій позиції тіла клаузи чиafterє хвостовим викликом, тож цикл сервера виконується в сталому обсязі стека. after T -> Body:Tобчислюється першим, до перегляду. Коли receive мав би чекати,Tмає бутиinfinityабо цілим числом у 0..4294967295 (мілісекунди), інакшеerror:timeout_value, після чого наступний receive переглядає з найстарішого повідомлення; повідомлення, що зіставляється одразу, ніколи його не перевіряє, як в OTP.after 0виконуєBody, щойно кожне повідомлення розглянуто. Скінченний тайм-аут починається, коли receive уперше чекає, і не перезапускається повідомленнями, що не зіставляються з жодною клаузою; коли він спливає,Bodyвиконується зі зв'язуваннями до receive, а наступний receive переглядає з найстарішого повідомлення. Повідомлення, що надійшло до спливання тайм-ауту, забирається. Receive лише зafter— це сон (ідіомаtimer:sleep/1).- Згенерований код: голова циклу зазирає до наступного нерозглянутого
повідомлення (
CLAUSE_receive_v1peek, у кореневий слот), вибір клаузи забирає зіставлене повідомлення (take) перед її тілом або пропускає незіставлене (skip) і повторює цикл; коли повідомлень не лишилося, цикл входить в очікування з тайм-аутом, яке відповідаєtrue(переглянути знову) абоfalse(тайм-аут:restart, потім тілоafter). - Виконавець тримає таймер для кожного процесу, що чекає зі скінченним тайм-аутом, у колесі таймерів (тайм-аути receive): таймер, що сплив, знову ставить процес у чергу. Тайм-аути вимірюються за монотонним годинником у мілісекундах і ніколи не спрацьовують раніше.
- Коли кожен процес чекає без тайм-ауту на повідомлення, яке ніхто не може
надіслати, програма чекає вічно, як і в OTP. Виклик із боку хоста
(
CLAUSE_invoke_v1), який мав би чекати, натомість завершується зbusy: під час нього жоден інший процес не виконується.
Зв'язки
Зв'язок (link) з'єднує два процеси в обох напрямках (Signals у кожному
контексті: зв'язані pid у порядку створення зв'язків і прапорець
trap_exit).
link(Pid)зв'язує того, хто викликає, з живим процесом і повертаєtrue; зв'язування із самим собою чи повторне зв'язування нічого не робить. Для процесу, що завершився, породжуєerror:noproc, або, коли той, хто викликає, перехоплює виходи, повертаєtrueі надсилає йому{'EXIT', Pid, noproc}, як це робить локальнийlink/1в OTP.unlink(Pid)видаляє зв'язок з обох боків; після повернення зв'язок не має ефекту.trueтакож тоді, коли зв'язку не було.spawn_link/1,3зв'язують новий процес із тим, хто викликає, до його виконання.- Коли процес завершується, кожен зв'язаний процес отримує сигнал виходу з його причиною виходу (виходи), і зв'язок зникає. Зв'язаним процесам сигнали надсилаються в порядку створення зв'язків.
Монітори
Монітор односпрямований: процес-монітор тримає його (за посиланням, у
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.
Зареєстровані імена
Виконавець тримає одну таблицю імен (атомів) до pid, а кожен процес —
власне ім'я (Signals::name).
register(Name, Pid)дає ім'я живому процесу:badargдляundefined, імені, що вже використовується, процесу, що вже має ім'я, процесу, що завершився, абоPid, що не є pid. Процес може зареєструвати сам себе.unregister(Name)звільняє ім'я (badarg, коли жоден процес його не має);whereis(Name)— це pid абоundefined;registered()перелічує імена в порядку таблиці атомів (порядок OTP теж не визначено).- Ім'я процесу звільняється, коли він завершується, до надсилання сигналів
його зв'язкам і моніторам, тож отримувач
'DOWN'чи'EXIT'може знову зареєструвати це ім'я, як в OTP.
Словник процесу
put/2, get/0,1, get_keys/0,1 і erase/0,1 поводяться як в OTP: put/2
і erase/1 повертають попереднє значення або undefined, erase/0
повертає кожну пару {Key, Value} і спорожнює словник, ключі зіставляються
за точною рівністю (1 і 1.0 — два ключі).
- Кожен процес має власний словник; запущений процес починає з порожнього.
- Записи — це слова купи процесу, що зберігаються в
ProcessDictionary(process_dictionary.hpp) і відвідуються як корені збирання (корені); вони звільняються разом із процесом. - Порядок
get/0іget_keys/0,1не визначено, як в OTP. Пошук лінійно переглядає записи; безпосередні ключі порівнюються за словом.put/2,get/1іerase/1беруть і повертають слова записів як є, не допускаючи жодного терма.
Таймери
start_timer/3,4 і send_after/3,4 (builtins/erlang/timers.cpp,
scheduler/timers.cpp) взводять таймер у колесі виконавця (тому, яке
використовують тайм-аути receive), що надсилає Msg
або {timeout, TimerRef, Msg} до pid чи зареєстрованого імені, коли
спрацьовує.
- Повідомлення копіюється під час запуску таймера в
MessageBuffer(буфери повідомлень), власний блок купи таймера, і знову копіюється в купу отримувача, коли таймер спрацьовує, як копіює надсилання: будь-який терм, зокрема локальні fun, передається незмінним. - Отримувач, що виконується на робочому потоці, отримує повідомлення, коли його квант закінчується. Зареєстроване ім'я розв'язується, коли таймер спрацьовує; ім'я, яке ніхто не має, або pid процесу, що завершився, відкидає повідомлення. Таймери, надіслані до pid, звільняються, коли цей процес завершується.
- Час — це відносні мілісекунди в 0..2^49-1 або з
{abs, true}абсолютнийerlang:monotonic_time(millisecond); будь-що інше —badarg. Термін за межами досяжності колеса взводиться знову, коли надходить цей слот. cancel_timer/1,2іread_timer/1,2повертають залишок мілісекунд абоfalse;{async, true}повертаєokі надсилає{cancel_timer, Ref, Left}({read_timer, ...}),{info, false}повертаєokбез результату.
Інтроспекція
process_info/1,2 (builtins/erlang/introspection.cpp,
scheduler/introspection.cpp) читає інший процес під блокуванням виконавця і
копіює його терми (повідомлення, словник) у купу того, хто викликає; процес,
що виконується на іншому робочому потоці, читається, щойно його квант
закінчиться. Pid без живого процесу дає undefined.
- Елементи:
registered_name([]окремо, коли ім'я не зареєстровано),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(слова купи, її фрагментів і binary поза купою, а також стека),reductions(0),priority(normal),error_handler,garbage_collection.process_info/1перелічує їх відstatusдоgarbage_collection. - Елементи OTP, яких Clause не веде (
current_function,initial_call,current_stacktrace, ...), породжуютьerror:{notsup, Item}; невідомі елементи —badarg. processes/0перелічує живі pid у порядку створення. Запущений процес успадковує group leader свого батька; перший процес очолює сам себе.group_leader/2для pid без живого процесу —badarg.garbage_collect/0,1,2запитує збирання в наступній точці безпеки цілі й повертаєtrue(false, якщо живого процесу немає); з{async, RequestId}повертаєasyncі надсилає{garbage_collect, RequestId, Result}.yield/0завершує квант часу при наступному вході у функцію.
Параметри запуску
Джерела: builtins/erlang/spawning.cpp, scheduler/aliases.cpp.
spawn_opt/2..5приймаютьlink,monitor,{monitor, MonitorOptions}і параметри налаштування{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}задає мінімальну купу дочірнього процесу, інші параметри налаштування перевіряються і приймаються, але не застосовуються. Вузол, відмінний відnonode@nohost, —badarg.spawn_request/1..5запускає процес одразу й надсилає{Tag, ReqId, ok, Pid}({Tag, ReqId, error, Reason}у разі збою,badoptдля недійсного параметра), як вимагають{reply_tag, Tag}і{reply, yes | no | error_only | success_only}; зmonitorідентифікатор запиту є посиланням монітора.spawn_request_abandon/1повертаєfalse: відповідь уже надіслано.hibernate(M, F, A)чекає, доки в поштовій скриньці не з'явиться повідомлення, потім викликаєM:F(A...)замість цього виклику; стек зберігається, тож результат повертається до того, хто викликав.
Псевдоніми
Псевдонім (alias) — це посилання, на яке можна надсилати повідомлення
(Alias ! Msg): alias/0,1 (explicit_unalias або reply для одного
повідомлення) чи монітор, створений з
{alias, explicit_unalias | demonitor | reply_demonitor} у monitor/3
(demonitor закінчується разом із монітором, reply_demonitor також
завершує монітор на першому повідомленні). unalias/1 завершує псевдонім
того, хто викликає. Надсилання до посилання, яке не є активним псевдонімом
або власник якого завершився, відкидається. Псевдонім reply споживається
лише тоді, коли його повідомлення доставлено, після будь-якого очікування
зайнятого власника. {tag, Tag} у monitor/3 замінює 'DOWN' у
повідомленні монітора; монітори портів параметрів не приймають.
Вузли
Програма Clause — це один нерозподілений вузол, nonode@nohost, і
відповідає як такий вузол OTP (builtins/erlang/node.cpp): nodes/0 дає
[], nodes(this | known) — [nonode@nohost], інші типи — []; nodes/2
дає {nonode@nohost, #{connection_id => undefined, node_type => this}} для запитаних ключів. is_alive/0 дає false, monitor_node/2,3
породжують error:notalive, а disconnect_node/1 повертає ignored.
Сигнали виходу
Кожен сигнал виходу надходить від процесу, що виконується (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);process_flag(min_heap_size, N)задає мінімальну купу з наступного збирання і повертає попереднє значення.- Процес, завершений сигналом, який він надіслав сам собі
(
exit(self(), kill)), розмотується повз коженcatch, обробникtryі тілоafter: сигнал не є винятком (CallError::exitedу каналі збоїв). exit/2іexit_signal/2повертаютьtrue; для процесу, що завершився, вони нічого не роблять. Посилання як адресат теж нічого не робить, навіть активний псевдонім.- Сигнал виходу, що завершує головний процес, завершує програму як
неперехоплений
exit(статус виходу): причинаnormalдає код виходу 0. - Коли купа отримувача відмовляє в причині виходу чи повідомленні
'EXIT', отримувач зазнає збою runtime (код виходу 70).
Порти
Порти описано в портах: порт зв'язаний із процесом, що його відкрив, бере участь у зв'язках, моніторах, сигналах виходу й зареєстрованих іменах, як процес, і спілкується зі своїм під'єднаним процесом повідомленнями.
Призупинення процесів
erlang:suspend_process/1,2 і erlang:resume_process/1
(scheduler/suspension.cpp): кожне призупинення — одне утримання цілі у
виконавці, тож призупинений процес знову виконується лише після відновлення
всіх призупинень від усіх процесів, що його призупинили; повідомлення й
сигнали й далі надходять (сигнал виходу й далі завершує його), а
process_info(Pid, status) повідомляє suspended. Завершення процесу, що
призупинив інші, відновлює його призупинення. Синхронне призупинення процесу
на іншому робочому потоці чекає завершення його кванту; asynchronous
повертає керування одразу, {asynchronous, Tag} також надсилає тому, хто
викликає, {Tag, suspended}, а unless_suspending відповідає false, якщо
той, хто викликає, вже призупинив цей процес. Призупинення чи відновлення
самого викликача, завершеного процесу, значення, що не є pid, або хибні
параметри дають badarg, як і відновлення процесу, якого викликач не
призупиняв.
Вбудовані функції
| Вбудована функція | Поведінка |
|---|---|
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), process_flag(min_heap_size, N), process_flag(save_calls, N) | Попереднє значення; badarg для іншого прапорця, не булевого значення або від'ємного чи нецілого розміру |
erlang:send(Dest, Msg), Dest ! Msg | Msg після його надсилання (повідомлення); send/2 не імпортується автоматично |
spawn(Node, ...), spawn_link(Node, ...), spawn_monitor(Node, ...) (/2, /4) | Цей вузол (nonode@nohost): як без Node. Інший вузол: марний pid неактивного вузла, як в OTP, процес, що одразу завершується з причиною noconnection, тож зв'язок чи монітор повідомляє noconnection; badarg, якщо вузол не атом |
erlang:send(Dest, Msg, Options) | ok або noconnect для повідомлення іншому вузлу з параметром noconnect; параметри nosuspend, noconnect, priority (звичайне повідомлення) |
send_nosuspend(Dest, Msg), send_nosuspend(Dest, Msg, [noconnect]) | true; false для іншого вузла з noconnect |
link(Pid, Options), exit(Dest, Reason, Options), exit_signal(Dest, Reason, Options) | Як без параметрів; Options — [] або [priority] (звичайний сигнал) |
process_flag(Pid, save_calls, N) | Попереднє налаштування іншого процесу (0..10000); зберігається, але Clause не зберігає викликів; інші прапорці — badarg |
processes_iterator(), processes_next(Iter) | Обхід живих процесів: {Pid, Iter1} або none; процес, запущений під час обходу, може з'явитися |
bump_reductions(N) | true для невід'ємного цілого числа; Clause не рахує редукцій |
erlang:suspend_process(Pid), erlang:suspend_process(Pid, Options), erlang:resume_process(Pid) | Див. призупинення процесів |
erlang:process_display(Pid, backtrace) | true після запису стека викликів живого процесу Pid у стандартний потік помилок, по одному Module:Function/Arity на рядок, починаючи з поточної функції; badarg для іншого типу чи завершеного процесу |
Новий процес стає в кінець черги робочого потоку свого батька
(робочі потоки); spawn одразу повертає його pid.
Clause