Уймин - про разработку
190 subscribers
3 photos
1 file
40 links
Авторский канал про backend-разработку. Подробнее - в закрепллённом сообщении.

Личиный аккаунт: @maksimuimin
Download Telegram
Паттерн early exit

Какие задачи стоят перед программистом, когда он реализует какую-то функцию? Обеспечить корректность алгоритма, при минимальной сложности кода. Именно в этом помогает паттерн 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
Сортировка подсчётом

В универе у меня была лаба: надо было отсортировать массив чисел, посчитать алгоритмическую сложность и количество сравнений.

📶 Обычно сортировки как выглядят? Проходишь по массиву, сравниваешь элементы друг с другом, меняешь местами по результатам сравнения. Если тебе нужно сравнивать каждый с каждым, то сложность алгоритма 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, про него имеет смысл говорить отдельно, т.к. грамотная утилизация процессора требует определённой квалификации от программиста.

🤔 Сколько процессора утилизирует этот код?
while (true) {}

В абсолютных числах - 1 поток. В процентах - зависит от машины, на которой код запущен. Надо поделить 1 на количество ядер процессора, помноженное на коэффициент гипертрединга. Подробнее писал в посте про утилизацию.

😮 Даже самый неоптимальный код по-умолчанию не может утилизировать все вычислительные ресурсы сервера, об этом должен позаботиться программист.

📖 Доступом к процессору, как и ко всем другим аппаратным ресурсам, управляет операционная система. Современные процессоры - многоядерные, ядра процессора могут выполнять вычисления параллельно, независимо друг от друга. Чтобы приложения могли использовать эту возможность, ОС предоставляет асинхронный 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 экран - задумайся. А не нарушил ли я принцип единственной ответственности?

🔟 Контролируй вложенность операторов. Когда ты вкладываешь if в другой if или в for, ты усложняешь код. Обрати внимание на количество отступов в начале строки. Если их больше 2-ух, это повод насторожиться и вспомнить про early exit.

Эти 10 правил формируют фундамент качества ПО и профессионального мастерства программиста. Пользуйся! 🔥

#hardskills #coding #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

Нужен для организации цепочек действий. Мидлвара оборачивает функцию дополнительной логикой:

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️⃣ Запустим в терминале бесконечный цикл:
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
Что под капотом у исключений

Пост для тех, кто раньше об этом не задумывался. Программисты думают об исключениях как о фиче языков программирования. ЯП поддерживает исключения, если предоставляет операторы try, catch, fially, throw. Но всё не так однозначно. Если бы ты писал свою реализацию исключений, как бы ты это сделал?

Рассмотрим фичи исключений.
- Исключение может прервать поток выполнения программы.
- Исключение должно раскручивать стек до ближайшего обработчика.
- Обработчик исключений, если установлен, должен выполниться незамедлительно при срабатывании исключения.
- Если обработчика нет, процесс должен аварийно завершиться.
- Исключения должны быть совместимы с фатальными ошибками ОС, такими как Segmentation fault.
- Обработчик исключений должен возвращать управление в точку после try/catch.

Исключения реализуются с помощью UNIX-сигналов и longjmp. Обе фичи есть стандартной библиотеке С, хотя готовых исключений там нет.

Наколдуем свои исключения.
🐱 Листинг кода: gist.github.com/maksimuimin/c14b066b0b454ac7a1962631c3a238bb
▶️ Результат выполнения:
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