Паттерн early exit
Какие задачи стоят перед программистом, когда он реализует какую-то функцию? Обеспечить корректность алгоритма, при минимальной сложности кода. Именно в этом помогает паттерн early exit.
📍 В чём суть: когда в вашем алгоритме появляется оператор ветвления (
ℹ️ Пример:
- Без early exit
- То же самое, но с early exit:
У early exit подхода есть ряд преимуществ:
1️⃣ Снижается вложенность операторов - это упрощает чтение кода.
2️⃣ Повышается фокус на основной путь выполнения функции - часто, когда дочитываешь длинную (вероятно, главную) ветвь выполнения кода, уже забываешь, зачем выше было ветвление и в чём смысл короткой ветви. Early exit позволяет быстро прочитать короткую ветвь и забыть о ней, сфокусировавшись на длинной ветви. В итоге, программисту нужно меньше “оперативной памяти” чтобы прочитать код 😊
3️⃣ Повышается корректность алгоритма - все граничные условия можно рассмотреть в самом начале функции, по моему опыту такой подход снижает количество ошибок.
4️⃣ Если в вашем языке нет
^ такой подход особенно популярен в ядре Linux: раз, два, три. Думаю, авторы этого кода что-то знают о программировании 😉
💡 Итого: при использовании операторов ветвления короткие ветви алгоритма следует обрабатывать перед длинными. Этот подход называется early exit. Он позволяет писать более простой и корректный код.
Ставь огонёк, если используешь early exit в своей работе 🔥
#hardskills #coding #pattern #bestpractice #codereading
Какие задачи стоят перед программистом, когда он реализует какую-то функцию? Обеспечить корректность алгоритма, при минимальной сложности кода. Именно в этом помогает паттерн early exit.
📍 В чём суть: когда в вашем алгоритме появляется оператор ветвления (
if
, switch
и т.п.), короткую ветвь вычислений надо обработать перед длинной ветвью.ℹ️ Пример:
- Без early exit
if user.isAuthorized() {
// Do
// some
// business
// logic
} else {
return errors.New("403 Anauthorized")
}
- То же самое, но с early exit:
if !user.isAuthorized() {
return errors.New("403 Anauthorized")
}
// Do
// some
// business
// logic
У early exit подхода есть ряд преимуществ:
1️⃣ Снижается вложенность операторов - это упрощает чтение кода.
2️⃣ Повышается фокус на основной путь выполнения функции - часто, когда дочитываешь длинную (вероятно, главную) ветвь выполнения кода, уже забываешь, зачем выше было ветвление и в чём смысл короткой ветви. Early exit позволяет быстро прочитать короткую ветвь и забыть о ней, сфокусировавшись на длинной ветви. В итоге, программисту нужно меньше “оперативной памяти” чтобы прочитать код 😊
3️⃣ Повышается корректность алгоритма - все граничные условия можно рассмотреть в самом начале функции, по моему опыту такой подход снижает количество ошибок.
4️⃣ Если в вашем языке нет
defer
'ов и сборки мусора, разновидность early exit упрощает корректную деаллокацию ресурсов. Пример:void foo() {
void *a = malloc(1024);
if (!a)
goto out_a;
void *b = malloc(1024);
if (!b)
goto out_b;
void *c = malloc(1024);
if (!c)
goto out_c;
/*
* Метки позволяют деаллоцировать только те ресурсы,
* которые были успешно аллоцированы
*/
free(c);
out_c:
free(b);
out_b:
free(a);
out_a:
return;
}
^ такой подход особенно популярен в ядре Linux: раз, два, три. Думаю, авторы этого кода что-то знают о программировании 😉
💡 Итого: при использовании операторов ветвления короткие ветви алгоритма следует обрабатывать перед длинными. Этот подход называется early exit. Он позволяет писать более простой и корректный код.
Ставь огонёк, если используешь early exit в своей работе 🔥
#hardskills #coding #pattern #bestpractice #codereading
Сортировка подсчётом
В универе у меня была лаба: надо было отсортировать массив чисел, посчитать алгоритмическую сложность и количество сравнений.
📶 Обычно сортировки как выглядят? Проходишь по массиву, сравниваешь элементы друг с другом, меняешь местами по результатам сравнения. Если тебе нужно сравнивать каждый с каждым, то сложность алгоритма
🎬 В общем-то, ничего нового, визуализации алгоритмов сортировки с помощью народных танцев на ютубе все смотрели. Вот пример за N квадрат, а вот за логарифм.
🤔 А что, если я скажу, что сортировку можно написать вообще без сравнений? На лабе я выдал примерно такой код:
^ позапускать можно здесь
🤯 У препода от такого глаза на лоб полезли. “И сколько тут сравнений?” - спросил он. “Ноль” - ответил я 😎 И кстати, оно работает за
🔥 В общем, думайте нестандартно, иногда даже самые глупые алгоритмы могут прекрасно работать. И обращайте внимание на ограничения, применимые ко входным данным. Это фундаментальная закономерность: чем больше ограничений можно применить ко входным данным, тем проще и эффективнее может быть алгоритм их обработки.
#theory #coding #algorithm
В универе у меня была лаба: надо было отсортировать массив чисел, посчитать алгоритмическую сложность и количество сравнений.
📶 Обычно сортировки как выглядят? Проходишь по массиву, сравниваешь элементы друг с другом, меняешь местами по результатам сравнения. Если тебе нужно сравнивать каждый с каждым, то сложность алгоритма
O(N²)
, это медленно 👎. Если реализовать более сложный алгоритм, каким-то образом бить массив на группы и работать уже с ними, то можно добиться сложности алгоритма O(N・log(N))
, это быстро 👍.🎬 В общем-то, ничего нового, визуализации алгоритмов сортировки с помощью народных танцев на ютубе все смотрели. Вот пример за N квадрат, а вот за логарифм.
🤔 А что, если я скажу, что сортировку можно написать вообще без сравнений? На лабе я выдал примерно такой код:
#include <stdint.h>
#include <string.h>
void count_sort(uint8_t *arr, size_t arr_len)
{
// У нас в аргументах массив байт. Каждый байт может принимать 256 разных значений
#define VALUES_CNT 256
// Аллоцируем массив счётчиков, для коротких типов данных можно в статической памяти
static uint64_t cnt[VALUES_CNT];
memset(cnt, 0, sizeof(uint64_t) * VALUES_CNT);
// Считаем вхождение каждого элемента в массив
for (size_t i = 0; i < arr_len; i++)
cnt[arr[i]]++;
// Переписываем массив
// Позиция в результирующем массиве
size_t i = 0;
// Проходим по массиву счётчиков
for (size_t b = 0; b < VALUES_CNT; b++) {
// Повторяем каждый элемент b в результирующем массиве c раз
for (uint64_t c = 0; c < cnt[b]; c++) {
arr[i] = (uint8_t)b;
i++;
}
}
}
^ позапускать можно здесь
🤯 У препода от такого глаза на лоб полезли. “И сколько тут сравнений?” - спросил он. “Ноль” - ответил я 😎 И кстати, оно работает за
O(N)
- это даже быстрее, чем за логарифм. Но ограничений, конечно, много: сортировать так можно только массивы с малой областью допустимых значений.🔥 В общем, думайте нестандартно, иногда даже самые глупые алгоритмы могут прекрасно работать. И обращайте внимание на ограничения, применимые ко входным данным. Это фундаментальная закономерность: чем больше ограничений можно применить ко входным данным, тем проще и эффективнее может быть алгоритм их обработки.
#theory #coding #algorithm
Потоки vs процессы: масштабирование по ядрам CPU
Это база - спрашиваю на каждом собеседовании 😉 С ростом нагрузки на систему становится критически важно грамотно утилизировать аппаратные ресурсы. Сегодня обсудим CPU, про него имеет смысл говорить отдельно, т.к. грамотная утилизация процессора требует определённой квалификации от программиста.
🤔 Сколько процессора утилизирует этот код?
В абсолютных числах - 1 поток. В процентах - зависит от машины, на которой код запущен. Надо поделить 1 на количество ядер процессора, помноженное на коэффициент гипертрединга. Подробнее писал в посте про утилизацию.
😮 Даже самый неоптимальный код по-умолчанию не может утилизировать все вычислительные ресурсы сервера, об этом должен позаботиться программист.
📖 Доступом к процессору, как и ко всем другим аппаратным ресурсам, управляет операционная система. Современные процессоры - многоядерные, ядра процессора могут выполнять вычисления параллельно, независимо друг от друга. Чтобы приложения могли использовать эту возможность, ОС предоставляет асинхронный API для параллельных вычислений, в основе которого лежит 2 концепции: потоки и процессы.
▶️ Процесс (process) - это запущенное приложение. У каждого процесса есть эксклюзивный доступ к ресурсам: аллоцированной памяти, файловым дескрипторам и т.д. Процессы максимально изолированы: они не имеют доступа к ресурсам друг друга и обладают независимым жизненным циклом. Любое взаимодействие между процессами требует написания кода с использованием специальных механизмов IPC (inter-process communication) - файлов, пайпов, сокетов, сигналов, и других.
➡️ Поток (thread) - это последовательность вычислений. У каждого процесса под капотом по-умолчанию есть 1 поток, все вычисления выполняются в нём последовательно. Ресурсы внутри процесса общие для всех потоков. Если кто-то открыл файл или аллоцировал память, все потоки могут этот ресурс прочитать и записать без необходимости использовать какие-то дополнительные механизмы.
🔀 По-настоящему параллельные вычисления, с утилизацией нескольких ядер CPU, можно организовать двумя способами:
- запустив дополнительные потоки
- создав дочерние процессы
☝️ Ключевая разница между двумя способами в разграничении доступа к ресурсам: у процессов эксклюзивные ресурсы, у потоков общие.
📈 Максимальной производительности можно достичь, когда кол-во запущенных потоков в системе равняется кол-ву потоков в архитектуре CPU. В этом случае каждый поток может выполняться непрерывно.
🤔 Зачем знать низкоуровневые API в 2024? У нас же есть языки программирования высокого уровня со всевозможными
⚠️ Дело в том, что ОС кроме процессов и тредов других асинхронных моделей не знает. Поэтому, если вам нужно больше 1 ядра CPU, понимание и использование низкоуровневых API необходимо. Например:
- В
- В
- В
- А в
🔥 Не забывайте про процессы и потоки - это фундамент, на котором стоят все параллельные вычисления. А параллельные вычисления - ключ к высокой производительности 😉
#theory #Linux #concurrency #tools #coding #pattern #highload
Это база - спрашиваю на каждом собеседовании 😉 С ростом нагрузки на систему становится критически важно грамотно утилизировать аппаратные ресурсы. Сегодня обсудим CPU, про него имеет смысл говорить отдельно, т.к. грамотная утилизация процессора требует определённой квалификации от программиста.
🤔 Сколько процессора утилизирует этот код?
while (true) {}
😮 Даже самый неоптимальный код по-умолчанию не может утилизировать все вычислительные ресурсы сервера, об этом должен позаботиться программист.
📖 Доступом к процессору, как и ко всем другим аппаратным ресурсам, управляет операционная система. Современные процессоры - многоядерные, ядра процессора могут выполнять вычисления параллельно, независимо друг от друга. Чтобы приложения могли использовать эту возможность, ОС предоставляет асинхронный API для параллельных вычислений, в основе которого лежит 2 концепции: потоки и процессы.
▶️ Процесс (process) - это запущенное приложение. У каждого процесса есть эксклюзивный доступ к ресурсам: аллоцированной памяти, файловым дескрипторам и т.д. Процессы максимально изолированы: они не имеют доступа к ресурсам друг друга и обладают независимым жизненным циклом. Любое взаимодействие между процессами требует написания кода с использованием специальных механизмов IPC (inter-process communication) - файлов, пайпов, сокетов, сигналов, и других.
➡️ Поток (thread) - это последовательность вычислений. У каждого процесса под капотом по-умолчанию есть 1 поток, все вычисления выполняются в нём последовательно. Ресурсы внутри процесса общие для всех потоков. Если кто-то открыл файл или аллоцировал память, все потоки могут этот ресурс прочитать и записать без необходимости использовать какие-то дополнительные механизмы.
🔀 По-настоящему параллельные вычисления, с утилизацией нескольких ядер CPU, можно организовать двумя способами:
- запустив дополнительные потоки
- создав дочерние процессы
☝️ Ключевая разница между двумя способами в разграничении доступа к ресурсам: у процессов эксклюзивные ресурсы, у потоков общие.
📈 Максимальной производительности можно достичь, когда кол-во запущенных потоков в системе равняется кол-ву потоков в архитектуре CPU. В этом случае каждый поток может выполняться непрерывно.
🤔 Зачем знать низкоуровневые API в 2024? У нас же есть языки программирования высокого уровня со всевозможными
async/await
, go func () { }
и другими высокоуровневыми асинхронными API.⚠️ Дело в том, что ОС кроме процессов и тредов других асинхронных моделей не знает. Поэтому, если вам нужно больше 1 ядра CPU, понимание и использование низкоуровневых API необходимо. Например:
- В
go
есть GOMAXPROCS- В
Node.js
есть --v8-pool-size- В
Python
есть threading и multiprocessing- А в
C/C++
можно использовать fork(2) и pthread_create(3) напрямую🔥 Не забывайте про процессы и потоки - это фундамент, на котором стоят все параллельные вычисления. А параллельные вычисления - ключ к высокой производительности 😉
#theory #Linux #concurrency #tools #coding #pattern #highload
10 советов сделают твой код лучше
1️⃣ Используй git и github. Они упростят внесение изменений и взаимодействие с другими программистами.
2️⃣ Напиши README. Его первым видят люди, открывая твой проект. Оставь хорошее впечатление, напиши, зачем проект нужен, как его собрать и запустить.
3️⃣ Используй early exit. Этот паттерн требует полчаса на изучение и улучшает читаемость кода в разы.
4️⃣ Проверяй входные параметры. Пользовательский ввод варьируется от случайного и бессмысленного до умышленно зловредного. Любые данные, полученные твоей программой, должны проверяться на корректность.
5️⃣ Обрабатывай ошибки. Почти любая функция может вернуть ошибку. Например, если программа получила некорректные входные параметры 😉. Есть 2 модели обработки ошибок - используй ту, которая больше нравится.
6️⃣ Оставляй комментарии. Только, пожалуйста, не для сверхразумов. Хорошие комменты идут проекту на пользу, плохие во вред.
7️⃣ Соблюдай принцип единственной ответственности. Каждый класс/программа/функция должна делать что-то одно и делать это хорошо. Этот принцип отражён в SOLID и философии UNIX. Научись разделять большую задачу на маленькие. Для каждой маленькой задачи сделай отдельный компонент в программе.
8️⃣ Контролируй длину строки. Чем длиннее строчки кода, тем сложнее его читать. Одна строка не должна быть длиннее половины экрана (80-100 символов). Это позволяет разместить на 1 экране 2 файла и читать их параллельно.
9️⃣ Контролируй длину функции. Если функция не помещается на 1 экран - задумайся. А не нарушил ли я принцип единственной ответственности?
🔟 Контролируй вложенность операторов. Когда ты вкладываешь
Эти 10 правил формируют фундамент качества ПО и профессионального мастерства программиста. Пользуйся! 🔥
#hardskills #coding #bestpractice
1️⃣ Используй git и github. Они упростят внесение изменений и взаимодействие с другими программистами.
2️⃣ Напиши README. Его первым видят люди, открывая твой проект. Оставь хорошее впечатление, напиши, зачем проект нужен, как его собрать и запустить.
3️⃣ Используй early exit. Этот паттерн требует полчаса на изучение и улучшает читаемость кода в разы.
4️⃣ Проверяй входные параметры. Пользовательский ввод варьируется от случайного и бессмысленного до умышленно зловредного. Любые данные, полученные твоей программой, должны проверяться на корректность.
5️⃣ Обрабатывай ошибки. Почти любая функция может вернуть ошибку. Например, если программа получила некорректные входные параметры 😉. Есть 2 модели обработки ошибок - используй ту, которая больше нравится.
6️⃣ Оставляй комментарии. Только, пожалуйста, не для сверхразумов. Хорошие комменты идут проекту на пользу, плохие во вред.
7️⃣ Соблюдай принцип единственной ответственности. Каждый класс/программа/функция должна делать что-то одно и делать это хорошо. Этот принцип отражён в SOLID и философии UNIX. Научись разделять большую задачу на маленькие. Для каждой маленькой задачи сделай отдельный компонент в программе.
8️⃣ Контролируй длину строки. Чем длиннее строчки кода, тем сложнее его читать. Одна строка не должна быть длиннее половины экрана (80-100 символов). Это позволяет разместить на 1 экране 2 файла и читать их параллельно.
9️⃣ Контролируй длину функции. Если функция не помещается на 1 экран - задумайся. А не нарушил ли я принцип единственной ответственности?
🔟 Контролируй вложенность операторов. Когда ты вкладываешь
if
в другой if
или в for
, ты усложняешь код. Обрати внимание на количество отступов в начале строки. Если их больше 2-ух, это повод насторожиться и вспомнить про early exit.Эти 10 правил формируют фундамент качества ПО и профессионального мастерства программиста. Пользуйся! 🔥
#hardskills #coding #bestpractice
Как писать обработчики ошибок?
Функции в программах возвращают ошибки. Что с ними делать?
1️⃣ Запиши в лог. Логи позволят расследовать влияние ошибки на сервис. Вместе с сообщением об ошибке запиши имя функции, которая её вернула:
Не добавляй в сообщение вводные слова: “failed foo()”, “foo() returned error” или “unable to call foo()”. Это не несёт новой информации и мешает чтению. Лучше в лог добавить аргументы, с которыми функция была вызвана. Это поможет в отладке.
2️⃣ Ограничь влияние на сервис. Ошибкам свойственно распространяться. Например, необработанное исключение в одном запросе аварийно завершает всю программу. Когда программа обслуживает нескольких клиентов, это нежелательное поведение. Лучше ограничить влияние ошибки одним запросом:
3️⃣ Разным ошибкам - разные обработчики. Ошибки отличаются между собой. В случае временных ошибок можно повторить попытку:
Часть ошибок может быть вариантом нормы. Например, ошибка
Ошибки можно сравнивать по типу или числовому коду при выборе обработчика. В примере выше, стандартные коды ошибок errno. ⚠️ Не следует сравнивать ошибки по текстовому сообщению. В нём может быть изменяемая часть.
Обработчики будут выглядеть одинаково вне зависимости от выбранной модели обработки ошибок. Пиши код так, чтобы он был готов к ошибкам. Это повысит отказоустойчивость программы и твой авторитет в глазах коллег 🔥
#hardskills #coding #errorhandling #logging #bestpractice
Функции в программах возвращают ошибки. Что с ними делать?
1️⃣ Запиши в лог. Логи позволят расследовать влияние ошибки на сервис. Вместе с сообщением об ошибке запиши имя функции, которая её вернула:
err = foo()
if err != nil {
log.Error("foo: %s", err)
return
}
Не добавляй в сообщение вводные слова: “failed foo()”, “foo() returned error” или “unable to call foo()”. Это не несёт новой информации и мешает чтению. Лучше в лог добавить аргументы, с которыми функция была вызвана. Это поможет в отладке.
2️⃣ Ограничь влияние на сервис. Ошибкам свойственно распространяться. Например, необработанное исключение в одном запросе аварийно завершает всю программу. Когда программа обслуживает нескольких клиентов, это нежелательное поведение. Лучше ограничить влияние ошибки одним запросом:
while (true) {
try {
ServeClient();
} catch (const Exception e) {
log.Error("ServeClient: %s", e);
// Больше ничего сделать не можем
// ¯\_(ツ)_/¯
}
}
3️⃣ Разным ошибкам - разные обработчики. Ошибки отличаются между собой. В случае временных ошибок можно повторить попытку:
int err = EINVAL;
// 3 попытки
for (int i = 0; i < 3; i++) {
err = do_some_stuff();
switch (err) {
case 0:
// Успех
return 0;
case EINTR:
// Временная ошибка, сработало прерывание
// Можно сразу пробовать ещё раз
continue;
case EAGAIN:
// Временная ошибка, сокет не готов к вводу/выводу
// Можно пробовать ещё раз, чуть позже
sleep(1); // TODO: лучше подписаться на событие вместо слипа
continue;
default:
// Непредвиденная ошибка
return err;
}
}
return err;
Часть ошибок может быть вариантом нормы. Например, ошибка
ENOENT
, файл не найден.Ошибки можно сравнивать по типу или числовому коду при выборе обработчика. В примере выше, стандартные коды ошибок errno. ⚠️ Не следует сравнивать ошибки по текстовому сообщению. В нём может быть изменяемая часть.
Обработчики будут выглядеть одинаково вне зависимости от выбранной модели обработки ошибок. Пиши код так, чтобы он был готов к ошибкам. Это повысит отказоустойчивость программы и твой авторитет в глазах коллег 🔥
#hardskills #coding #errorhandling #logging #bestpractice
Паттерн middleware
Нужен для организации цепочек действий. Мидлвара оборачивает функцию дополнительной логикой:
Мидлвары умеют выстраиваться в цепочки. Попробуй поиграть с кодом из примера, вот так выглядит:
Когда цепочка становится длинной, мидлвары можно хранить в списке и применять к функциям по запросу.
Патерн middleware улучшает модульность кода, с ним дополнительную логику просто подключить и использовать повторно. Такой подход популярен в HTTP-серверах - там у обработчиков запросов одинаковая сигнатура, это позволяет использовать одну мидлвару для всех обработчиков. Такой подход применим везде, где много функций с одинаковой сигнатурой.
Выноси в мидлвары:
- проверку авторизации;
- логирование запросов и ответов;
- замеры метрик latency, throughput, errors rate;
- обработку исключений;
- проверку ключей идемпотентости;
- ретраи и фейловеры.
Мидлвары добавляют одну и ту же логику в разных местах без копипасты. Пользуйтесь 🔥
#hardskills #coding #pattern
Нужен для организации цепочек действий. Мидлвара оборачивает функцию дополнительной логикой:
package main
import "fmt"
type MyFunc func(string)
type Middleware func(next MyFunc) MyFunc
func Hello(name string) {
fmt.Printf("Hello, %s\n", name)
}
func GentleMiddleware(next MyFunc) MyFunc {
return func(name string) {
fmt.Println("Greetings!")
next(name)
fmt.Println("Goodbye, see you soon")
}
}
func main() {
GentleMiddleware(Hello)("world")
/* Greetings!
* Hello, world
* Goodbye, see you soon
*/
}
Мидлвары умеют выстраиваться в цепочки. Попробуй поиграть с кодом из примера, вот так выглядит:
GentleMiddleware(GentleMiddleware(Hello))("world")
Когда цепочка становится длинной, мидлвары можно хранить в списке и применять к функциям по запросу.
Патерн middleware улучшает модульность кода, с ним дополнительную логику просто подключить и использовать повторно. Такой подход популярен в HTTP-серверах - там у обработчиков запросов одинаковая сигнатура, это позволяет использовать одну мидлвару для всех обработчиков. Такой подход применим везде, где много функций с одинаковой сигнатурой.
Выноси в мидлвары:
- проверку авторизации;
- логирование запросов и ответов;
- замеры метрик latency, throughput, errors rate;
- обработку исключений;
- проверку ключей идемпотентости;
- ретраи и фейловеры.
Мидлвары добавляют одну и ту же логику в разных местах без копипасты. Пользуйтесь 🔥
#hardskills #coding #pattern
UNIX - сигналы
Рассмотрим UNIX сигналы - механизм ОС для управления процессами. Сигналы используются для:
- системного администрирования;
- обработки исключений;
- межпроцессного взаимодействия.
Я расскажу, что такое сигналы, как написать свой обработчик и как применить их на практике.
Сигнал уведомляет процесс о наступлении события. При получении сигнала процесс вызывает обработчик сигнала, прервав нормальный поток выполнения. Обычно, нормальный поток выполнения продолжается после вызова обработчика с того места, где был прерван. Но бывают исключения. Например:
1️⃣ Запустим в терминале бесконечный цикл:
2️⃣ Нажмём
Послать сигнал можно несколькими способами:
- процессу - с помощью программы или сисколла
- группе процессов - с помощью библиотечного вызова
- отдельному потоку - с помощью сисколла
Обработчик сигнала выполняется вне очереди, даже в однопоточном приложении. Сигналы - самый простой способ выполнить задачу вне очереди. Список стандартных сигналов и обработчиков по умолчанию ищи в мануале.
Программист может определить свои обработчики. Для управления обработчиками используй вызов sigaction. Он может установить кастомный обработчик, вернуть обработчик по умолчанию, включить игнор сигнала.
⚠️ Будь осторожен при написании кастомных обработчиков - можно легко получить неопределённое поведение. Чтобы этого избежать, используй в обработчиках только
В заключение, рассмотрим кейсы использования сигналов:
- Nginx:
- Tarantool:
- Kubernetes: при завершении пода посылает
Сигналы незаменимы в разработке под Linux. Надеюсь, в твоём тулсете стало на 1 инструмент больше 🔥
#theory #coding #tools #Linux #concurrency
===
Мои любимые посты в канале t.me/uimindev/37
Рассмотрим UNIX сигналы - механизм ОС для управления процессами. Сигналы используются для:
- системного администрирования;
- обработки исключений;
- межпроцессного взаимодействия.
Я расскажу, что такое сигналы, как написать свой обработчик и как применить их на практике.
Сигнал уведомляет процесс о наступлении события. При получении сигнала процесс вызывает обработчик сигнала, прервав нормальный поток выполнения. Обычно, нормальный поток выполнения продолжается после вызова обработчика с того места, где был прерван. Но бывают исключения. Например:
1️⃣ Запустим в терминале бесконечный цикл:
while true; do echo "Looping..."; done
2️⃣ Нажмём
Ctrl + C
- цикл завершится. Эта комбинация клавиш отправляет сигнал SIGINT
. Обработчик по умолчанию для этого сигнала - завершить процесс, поэтому нормальный поток в этом случае не был продолжен.Послать сигнал можно несколькими способами:
- процессу - с помощью программы или сисколла
kill
;- группе процессов - с помощью библиотечного вызова
killpg
;- отдельному потоку - с помощью сисколла
tkill
или библиотечного вызова pthread_kill
.Обработчик сигнала выполняется вне очереди, даже в однопоточном приложении. Сигналы - самый простой способ выполнить задачу вне очереди. Список стандартных сигналов и обработчиков по умолчанию ищи в мануале.
Программист может определить свои обработчики. Для управления обработчиками используй вызов sigaction. Он может установить кастомный обработчик, вернуть обработчик по умолчанию, включить игнор сигнала.
⚠️ Будь осторожен при написании кастомных обработчиков - можно легко получить неопределённое поведение. Чтобы этого избежать, используй в обработчиках только
async-signal-safe
функции стандартной библиотеки. Список безопасных функций можно найти здесь.В заключение, рассмотрим кейсы использования сигналов:
- Nginx:
SIGUSR1
ротирует логи, SIGUSR2
запускает graceful restart, SIGQUIT
запускает graceful shutdown, SIGHUP
перечитывает конфиги - см. тут;- Tarantool:
SIGUSR1
откладывает снапшот базы данных - см. тут;- Kubernetes: при завершении пода посылает
SIGTERM
для запуска graceful shutdown и SIGKILL
для принудительного завершения - см. тут.Сигналы незаменимы в разработке под Linux. Надеюсь, в твоём тулсете стало на 1 инструмент больше 🔥
#theory #coding #tools #Linux #concurrency
===
Мои любимые посты в канале t.me/uimindev/37
Что под капотом у исключений
Пост для тех, кто раньше об этом не задумывался. Программисты думают об исключениях как о фиче языков программирования. ЯП поддерживает исключения, если предоставляет операторы
Рассмотрим фичи исключений.
- Исключение может прервать поток выполнения программы.
- Исключение должно раскручивать стек до ближайшего обработчика.
- Обработчик исключений, если установлен, должен выполниться незамедлительно при срабатывании исключения.
- Если обработчика нет, процесс должен аварийно завершиться.
- Исключения должны быть совместимы с фатальными ошибками ОС, такими как Segmentation fault.
- Обработчик исключений должен возвращать управление в точку после
Исключения реализуются с помощью UNIX-сигналов и
Наколдуем свои исключения.
🐱 Листинг кода: gist.github.com/maksimuimin/c14b066b0b454ac7a1962631c3a238bb
▶️ Результат выполнения:
Практические выводы.
📌 ОС сообщает о фатальных ошибках с помощью UNIX-сигналов. Такие ошибки можно обработать и как исключения, и как сигналы. Обработка фатальных ошибок - хорошая практика безопасного программирования, даже если ваш ЯП не поддерживает исключения. Пример из реального мира, почтовый сервер Exim: github.com/Exim/exim/blob/exim-4.97.1/src/src/receive.c#L3805
📌
Исключения - сложный механизм. Я предпочитаю модель обработки ошибок на основе возвращаемых значений - там всё просто. Однако, эта модель никак не обрабатывает фатальные ошибки ОС. Поэтому, на практике используется комбинация двух моделей: возвращаемые значения для стандартных сбоев и исключения/сигналы для фатальных ошибок. Это хороший компромисс между простотой и отказоустойчивостью 🔥
#theory #coding #Linux #errorhandling
===
Мои любимые посты в канале t.me/uimindev/37
Пост для тех, кто раньше об этом не задумывался. Программисты думают об исключениях как о фиче языков программирования. ЯП поддерживает исключения, если предоставляет операторы
try
, catch
, fially
, throw
. Но всё не так однозначно. Если бы ты писал свою реализацию исключений, как бы ты это сделал?Рассмотрим фичи исключений.
- Исключение может прервать поток выполнения программы.
- Исключение должно раскручивать стек до ближайшего обработчика.
- Обработчик исключений, если установлен, должен выполниться незамедлительно при срабатывании исключения.
- Если обработчика нет, процесс должен аварийно завершиться.
- Исключения должны быть совместимы с фатальными ошибками ОС, такими как Segmentation fault.
- Обработчик исключений должен возвращать управление в точку после
try/catch
.Исключения реализуются с помощью UNIX-сигналов и
longjmp
. Обе фичи есть стандартной библиотеке С, хотя готовых исключений там нет.Наколдуем свои исключения.
▶️ Результат выполнения:
Try 1
Got signal: Aborted
Exception 1
Try 2
Try 3
Got signal: Segmentation fault
Exception 3
Практические выводы.
📌 ОС сообщает о фатальных ошибках с помощью UNIX-сигналов. Такие ошибки можно обработать и как исключения, и как сигналы. Обработка фатальных ошибок - хорошая практика безопасного программирования, даже если ваш ЯП не поддерживает исключения. Пример из реального мира, почтовый сервер Exim: github.com/Exim/exim/blob/exim-4.97.1/src/src/receive.c#L3805
📌
longjmp
работает как goto
, но может передавать управление между функциями. Позволяет реализовать оператор throw
без сигналов. Пример из реального мира, функция error
в Lua: pgl.yoyo.org/luai/i/lua_errorИсключения - сложный механизм. Я предпочитаю модель обработки ошибок на основе возвращаемых значений - там всё просто. Однако, эта модель никак не обрабатывает фатальные ошибки ОС. Поэтому, на практике используется комбинация двух моделей: возвращаемые значения для стандартных сбоев и исключения/сигналы для фатальных ошибок. Это хороший компромисс между простотой и отказоустойчивостью 🔥
#theory #coding #Linux #errorhandling
===
Мои любимые посты в канале t.me/uimindev/37
Please open Telegram to view this post
VIEW IN TELEGRAM