Процеси в Elixir ізольовані й типово нічого не поділяють між собою. Коли незвʼязаний дочірній процес аварійно завершується, його батьківський процес від цього не страждає.
Цю поведінку можна змінити, звʼязавши процеси один з одним. Якщо два процеси звʼязані, збій в одному з них пошириться на інший процес. Звʼязки двонапрямні.
Процеси можна породжувати вже звʼязаними з процесом, який їх викликає, за допомогою spawn_link/1 (це атомарна операція), або звʼязати їх пізніше через Process.link/1.
Звʼязування процесів стає в пригоді під час розпаралеленої роботи, коли кожен фрагмент роботи не варто продовжувати, якщо інший фрагмент не завершився.
Звʼязування також можна використовувати для нагляду за процесами. Якщо процес перехоплює виходи, він не аварійно завершиться, коли аварійно завершиться звʼязаний із ним процес. Натомість він отримає повідомлення про збій. Це дає йому змогу коректно обробити збій, наприклад перезапустивши процес, який зазнав збою.
Процес можна налаштувати на перехоплення виходів, викликавши Process.flag(:trap_exit, true). Зауважмо, що Process.flag/2 повертає старе значення прапорця, а не нове.
Повідомлення, яке надійде процесу, якщо звʼязаний процес зазнає збою, відповідатиме шаблону {:EXIT, from, reason}, де from - це PID. Якщо reason - це будь-що інше, ніж атом :normal, це означає, що процес аварійно завершився або його примусово вбили.
Задачі - це процеси, призначені виконувати одну конкретну операцію. Зазвичай вони не спілкуються з іншими процесами, але можуть повернути результат процесу, який запустив задачу.
Задачі часто використовують, щоб розпаралелити роботу.
async/await
Щоб запустити задачу, використаймо Task.async/1. Вона приймає анонімну функцію як аргумент і виконує її в новому процесі, звʼязаному з процесом, який викликає. Вона повертає структуру %Task{}.
Щоб отримати результат виконання, передаймо структуру %Task{} у Task.await/2. Вона дочекається завершення задачі й поверне її результат. Другий аргумент - це час очікування в мілісекундах, типово 5000.
Зауважмо, що між запуском задачі та очікуванням її результату процес, який запустив задачу, не блокується й може виконувати інші операції.
Для будь-якої задачі, запущеної через Task.async/1, потрібно дочекатися результату, бо вона надішле повідомлення процесу, який викликає. Task.await/2 можна викликати для кожної задачі лише один раз.
start/start_link
Якщо потрібно запустити задачу лише заради побічних ефектів, використаймо Task.start/1 або Task.start_link/1. Task.start/1 запустить задачу, не звʼязану з процесом, який викликає, а Task.start_link/1 запустить задачу, звʼязану з процесом, який викликає. Обидві функції повертають кортеж {:ok, pid}.
Наша робота в Instruments of Texas над експериментальним калькулятором зі зворотним польським записом (RPN) триває. Наша команда створила кілька прототипів, які мають пройти ретельну перевірку, щоб вибрати найкращий з них, придатний для масового виробництва.
Нам потрібно провести два типи перевірок.
По-перше, перевірка надійності, яка виявлятиме вхідні дані, для яких калькулятор, що проходить перевірку, або аварійно завершується, або відповідає недостатньо швидко. Щоб ізолювати збої, обчислення для кожних вхідних даних потрібно запускати в окремому процесі. Звʼязування та перехоплення виходів у процесі-викликачі дають змогу визначити, чи завершилося обчислення успішно, а чи зі збоєм.
По-друге, перевірка правильності, яка перевірятиме, чи для заданих вхідних даних результат, повернений калькулятором, відповідає очікуваному. Лише калькулятори, які вже пройшли перевірку надійності, будуть проходити перевірку правильності, тож збоїв можна не побоюватися. Однак операції варто запускати паралельно, щоб пришвидшити процес, а це ідеальний випадок для асинхронних завдань.
Реалізуйте функцію RPNCalculatorInspection.start_reliability_check/2. Вона має приймати 2 аргументи: функцію (калькулятор) і вхідні дані для калькулятора. Вона має повертати мапу, яка містить вхідні дані та PID створеного процесу.
Створений процес має викликати задану функцію-калькулятор із заданими вхідними даними. Цей процес має бути звʼязаний із процесом-викликачем.
RPNCalculatorInspection.start_reliability_check(fn _ -> 0 end, "2 3 +")
# => %{input: "2 3 +", pid: #PID<0.169.0>}
Реалізуйте функцію RPNCalculatorInspection.await_reliability_check_result/2. Вона має приймати два аргументи. Перший аргумент - це мапа з вхідними даними перевірки надійності та PID процесу, який виконує перевірку надійності для цих вхідних даних, у тому вигляді, у якому їх повертає RPNCalculatorInspection.start_reliability_check/2. Другий аргумент - це мапа, яка слугує акумулятором результатів перевірок надійності з різними вхідними даними.
Функція має чекати на повідомлення про вихід.
Якщо вона отримує повідомлення про вихід ({:EXIT, from, reason}) із причиною :normal від того самого процесу, який виконує перевірку надійності, вона має повернути мапу результатів зі значенням :ok, доданим під ключем input.
Якщо вона отримує повідомлення про вихід з іншою причиною від того самого процесу, який виконує перевірку надійності, вона має повернути мапу результатів зі значенням :error, доданим під ключем input.
Якщо протягом 100 мс вона не отримає жодного повідомлення, що відповідає цим критеріям, вона має повернути мапу результатів зі значенням :timeout, доданим під ключем input.
# when an exit message is waiting for the process in its inbox
send(self(), {:EXIT, pid, :normal})
RPNCalculatorInspection.await_reliability_check_result(
%{input: "5 7 -", pid: pid},
%{}
)
# => %{"5 7 -" => :ok}
# when there are no messages in the process inbox
RPNCalculatorInspection.await_reliability_check_result(
%{input: "3 2 *", pid: pid},
%{"5 7 -" => :ok}
)
# => %{"5 7 -" => :ok, "3 2 *" => :timeout}
Реалізуйте функцію RPNCalculatorInspection.reliability_check/2. Вона має приймати 2 аргументи: функцію (калькулятор) і масив вхідних даних для калькулятора.
Для кожних вхідних даних із масиву вона має запустити перевірку надійності в новому звʼязаному процесі за допомогою start_reliability_check/2. Потім для кожного запущеного в такий спосіб процесу вона має дочекатися його результатів за допомогою await_reliability_check_result/2.
Перш ніж запускати будь-які процеси, функція має увімкнути для поточного процесу перехоплення виходів, щоб мати змогу отримувати повідомлення про вихід. Після цього вона має скинути цей прапорець до початкового значення.
Функція має повернути мапу з результатами перевірок надійності для всіх вхідних даних.
fake_broken_calculator = fn input ->
if String.ends_with?(input, "*"), do: raise("oops")
end
inputs = ["2 3 +", "10 3 *", "20 2 /"]
RPNCalculatorInspection.reliability_check(fake_broken_calculator, inputs)
# => %{
# "2 3 +" => :ok,
# "10 3 *" => :error,
# "20 2 /" => :ok
# }
Реалізуйте функцію RPNCalculatorInspection.correctness_check/2. Вона має приймати 2 аргументи: функцію (калькулятор) і масив вхідних даних для калькулятора.
Для кожних вхідних даних із масиву вона має запустити асинхронне завдання, яке викличе калькулятор із цими вхідними даними. Потім для кожного запущеного в такий спосіб завдання вона має дочекатися його результатів протягом 100 мс.
fast_cheating_calculator = fn input -> 14 end
inputs = ["13 1 +", "50 2 *", "1000 2 /"]
RPNCalculatorInspection.correctness_check(fast_cheating_calculator, inputs)
# => [14, 14, 14]
Зареєструйтеся на Exercism, щоб вивчати й опановувати Elixir, а також 58 концепцій168 вправ та справжнє наставництво від людей, і все це безкоштовно.