27 KiB
u_async — Центральный цикл событий (event loop)
1. Назначение
u_async — центральный планировщик проекта uTun. Каждый поток, которому нужна асинхронная обработка сокетов, таймеров и отложенных задач, создаёт свой экземпляр struct UASYNC и запускает uasync_mainloop() / uasync_poll(). Модуль объединяет:
- Сокеты — мониторинг fd/socket на чтение/запись/ошибки через Linux epoll (предпочтительно) с fallback на poll/select.
- Таймеры — однократные таймеры на базе
timeout_heap(min-heap) иmemory_pool. Аргумент задаётся в единицах 0.1 мс; положительный абсолютный срок округляется вверх до миллисекунды. - Отложенное выполнение — FIFO-очередь
uasync_call_soon. Callback может выполниться в текущемuasync_poll, при следующем проходе обработки этой очереди. - Межпоточную связь —
uasync_post/uasync_post_reservedпубликуют callback для потока, обслуживающего экземпляр (wakeup через eventfd/pipe/UDP-сокет).
На Linux используется epoll, на FreeBSD/Windows — poll/select. Платформенная абстракция прозрачна для вызывающего кода.
2. Как пользоваться
Правила
- Один обслуживающий поток на экземпляр. Независимые экземпляры могут работать в разных потоках. Сокеты, таймеры и обработка очередей конкретного экземпляра принадлежат его потоку. Из другого потока разрешены
uasync_post,uasync_post_reserved,uasync_wakeupиuasync_stopпри согласованном времени жизни экземпляра. - Никаких sleep/usleep. Если поток заблокирован на sleep, цикл событий стоит. Все ожидания — только через таймеры
uasync_set_timeout. - Закрытие из callback. Можно удалять сокет и освобождать его пользовательский контекст из callback. Перед следующим callback модуль повторно проверяет регистрацию, поколение и разрешённые события. Для завершения цикла вызывайте
uasync_stop;uasync_destroyвыполняйте после возврата изuasync_poll/uasync_mainloop, вне callbacks. - Указатель
user_argпередаётся во все callback и позволяет передать контекстную структуру (например,UTUN_INSTANCE). - Владение контекстом.
user_argне копируется и автоматически не освобождается. При отмене или уничтожении ожидающей задачи её callback не вызывается; контекст освобождает владелец. Передdestroyзавершите producers, которые могут обращаться к экземпляру или публиковать задачи. - Статусы
runningиstopнезависимы. Послеcreateоба равны 0, API уже доступен.mark_running/mark_stoppedтолько выставляют статус владельца; цикл и регистрацию задач не меняют.stopустанавливает отдельный флаг выхода изmainloop, который автоматически не сбрасывается.
Типовой сценарий
// 1. Создать uasync
struct UASYNC* ua = uasync_create();
if (!ua) return;
// 2. Зарегистрировать сокет
void* sock_id = uasync_add_socket(ua, fd, my_read_cb, NULL, NULL, "my_fd", my_ctx);
// Вместо предыдущей строки для socket_t:
// void* sock_id = uasync_add_socket_t(ua, sock, my_read_cb_sock, my_write_cb_sock, NULL, "my_socket", my_ctx);
// 3. Зарегистрировать таймер (timebase = 0.1 мс, т.е. timeout_tb=100 = 10 мс)
void* t_id = uasync_set_timeout(ua, 100, my_ctx, my_timer_cb, "my_timer");
// 4. Отменить таймер (обязательно обнулить дескриптор!)
uasync_cancel_timeout(ua, t_id);
t_id = NULL;
// 5. Отложенное выполнение при обработке FIFO call_soon
void* soon_id = uasync_call_soon(ua, my_ctx, deferred_cb);
// 6. Динамически включить/отключить мониторинг записи на сокете
uasync_set_socket_write(ua, sock_id, 1); // включить
uasync_set_socket_write(ua, sock_id, 0); // отключить
// 7. Запустить главный цикл (блокирующий, выход через uasync_stop)
uasync_mainloop(ua);
// 8. Вместо mainloop можно вызывать отдельные шаги:
// uasync_poll(ua, 500); // системное ожидание до 50 мс, время callbacks этим не ограничено
// 9. Завершение
uasync_destroy(ua, 1); // закрыть оставшиеся зарегистрированные fd и внутренние ресурсы
Таймеры: правильный паттерн
void* reconnect_timer = NULL; // дескриптор всегда обнуляем
// При запуске:
if (!reconnect_timer) {
reconnect_timer = uasync_set_timeout(ua, 5000, ctx, reconnect_cb, "reconnect");
}
// В callback или при отмене:
void reconnect_cb(void* arg) {
reconnect_timer = NULL; // сработал — обнулили
// ...
}
// При ручной отмене:
if (reconnect_timer) {
uasync_cancel_timeout(ua, reconnect_timer);
reconnect_timer = NULL;
}
Межпоточное взаимодействие (uasync_post)
// Из другого потока:
void notify_main(void* arg) {
UTUN_INSTANCE* u = arg;
// Работаем в потоке, обслуживающем u->ua: здесь можно регистрировать таймеры и сокеты.
uasync_set_timeout(u->ua, 10, u, handle_work, "work");
}
void thread_func(UTUN_INSTANCE* u) {
// …
uasync_post(u->ua, notify_main, u);
}
// Отдельный uasync_memsync перед post не требуется: публикация и получение задачи используют posted_lock.
Публикация не продлевает время жизни ua или arg и не проверяет running / stop.
Обычный uasync_post выделяет posted_task; при OOM пишет ошибку в лог и не ставит задачу, статуса возврата нет.
Для завершения worker без выделения памяти заранее выделите posted_task через u_malloc, заполните callback и arg
и один раз передайте в uasync_post_reserved. После передачи производитель к задаче не обращается.
Задачу освобождает uasync после исполнения, успешной отмены или при уничтожении без вызова callback; arg не освобождается.
Ошибка wakeup оставляет уже опубликованную задачу в очереди; обе функции публикации не возвращают эту ошибку.
Отмена через uasync_cancel_post выполняется в потоке владельца вне callbacks после завершения производителя.
ERR_OK означает, что задача найдена в ожидающей очереди и освобождена; ERR_FAIL — не найдена.
Порция, уже взятая на исполнение, в этой очереди не находится. uasync_memsync лишь захватывает и отпускает
posted_lock; он не ждёт исполнения задач и не заменяет согласование времени жизни контекста.
Получение времени
uint64_t now_tb = get_time_tb(); // timebase 0.1 мс (монотонные часы)
uint64_t now_us = get_time_us(); // микросекунды (для burst-измерений)
3. API
Жизненный цикл
| Функция | Описание |
|---|---|
uasync_create() |
Создать готовый к регистрации и опросу экземпляр с running=0, stop=0. Возвращает указатель или NULL при ошибке инициализации. |
uasync_destroy(ua, close_fds) |
Уничтожить вне callbacks после возврата из poll/mainloop и завершения producers. При close_fds != 0 закрывает оставшиеся зарегистрированные fd; внутренние ресурсы закрывает всегда. Ожидающие задачи и таймеры удаляет без callbacks и без освобождения arg. Проверяет баланс внутренних аллокаций (abort при несовпадении). NULL допустим. |
uasync_mark_running(ua) / uasync_mark_stopped(ua) |
Атомарно выставить running=1/0. Не меняют stop, не управляют циклом и не ограничивают API. |
uasync_is_running(ua) |
Атомарно прочитать статус владельца; для NULL возвращает 0. |
uasync_stop(ua) |
Атомарно выставить stop=1 и попытаться разбудить ожидание. Допустим из другого потока при живом экземпляре. Текущий poll, оставшуюся обработку событий и callbacks не прерывает; running не меняет. |
uasync_mainloop(ua) |
Цикл while(!stop) uasync_poll(ua, -1). При уже установленном stop сразу возвращается, флаг не сбрасывает. |
uasync_poll(ua, timeout_tb) |
Один проход обработки событий. Отрицательное значение снимает пользовательский предел системного ожидания; 0 запрещает блокирующее ожидание; положительное задаёт предел в 0.1 мс с округлением вверх до миллисекунд, сокращаемый таймерами. Время callbacks этим пределом не ограничено. После stop ожидание неблокирующее. |
Таймеры (timebase = 0.1 мс)
| Функция | Описание |
|---|---|
uasync_set_timeout(ua, timeout_tb, arg, cb, name) |
Однократный таймер: timeout_tb >= 0, callback обязателен. Возвращает handle или NULL при неверных аргументах/ошибке ресурсов. arg и строка name сохраняются по указателю; ограничения имени в 15 символов нет. |
uasync_cancel_timeout(ua, t_id) |
Отменить ещё живой таймер этого экземпляра: ERR_OK / ERR_FAIL. При успехе узел помечается отменённым; освобождение отложено до очистки heap или destroy. После успеха обнулить handle. |
Для положительного timeout_tb абсолютный срок в миллисекундах равен
ceil((now_us + timeout_tb * 100) / 1000), где now_us взят при регистрации.
При нулевом интервале срок равен floor(now_us / 1000) и таймер готов к ближайшей обработке heap.
Callback синхронно из set_timeout не вызывается; его исполнение может задержаться из-за ожидания или других callbacks.
Строка name должна оставаться доступной до освобождения узла, включая отложенную очистку после отмены.
В начале callback сохранённый handle нужно обнулить. После исполнения, освобождения или destroy старый указатель
использовать нельзя: безопасная ошибка при повторной отмене не гарантируется.
Немедленное выполнение (FIFO)
| Функция | Описание |
|---|---|
uasync_call_soon(ua, arg, cb) |
Добавить callback в FIFO для отложенного исполнения. Возвращает handle или NULL при неверных аргументах/ошибке ресурсов; узел выделяется из timeout_pool. |
uasync_call_soon_cancel(ua, t_id) |
Занулить callback ещё живого узла этого экземпляра за O(1); узел освобождается при обработке очереди/destroy. ERR_OK при отмене, ERR_FAIL при NULL/чужом экземпляре. После успеха обнулить handle. |
uasync_drain_immediate(ua) |
Синхронно обработать все порции call_soon до опустошения, включая новые callbacks. Не обрабатывает posted tasks, сокеты или heap таймеров. Постоянный самоперезапуск callback может не дать функции вернуться. |
Один проход обработки FIFO берёт исходную порцию. Обычный успешный poll обрабатывает FIFO в начале,
затем после ожидания и обработки I/O/posted tasks, перед проверкой heap таймеров.
Поэтому callback из обработчика сокета или из первой порции call_soon может выполниться в том же poll.
Ошибка ожидания может прервать проход до второй обработки FIFO. Гарантии «следующая итерация poll/mainloop» нет.
При повторной отмене call_soon до обработки узла возможен ERR_OK; после освобождения узла старый handle недействителен.
Сокеты
| Функция | Описание |
|---|---|
uasync_add_socket(ua, fd, r_cb, w_cb, e_cb, name, arg) |
Зарегистрировать fd. Callbacks могут быть NULL; name и arg заимствуются. Возвращает handle либо NULL при неверном fd, дублирующей регистрации или ошибке ресурсов/backend. |
uasync_add_socket_t(ua, sock, r_cb, w_cb, e_cb, name, arg) |
То же для socket_t; callbacks получают значение сокета. |
uasync_remove_socket(ua, s_id) |
Удалить регистрацию по актуальному handle, не закрывая fd и не освобождая arg. Слот становится доступным для повторного использования. ERR_OK / ERR_FAIL. |
uasync_remove_socket_t(ua, sock) |
Удалить регистрацию типа SOCK по значению socket_t, не закрывая сокет. ERR_OK / ERR_FAIL. |
uasync_set_socket_read(ua, s_id, enable) |
По актуальному handle включить (enable != 0) / отключить (0) мониторинг чтения для заданного callback. ERR_OK при успехе, ERR_FAIL при неверном handle/ошибке backend. |
uasync_set_socket_write(ua, s_id, enable) |
То же для мониторинга записи. |
uasync_lookup_socket(ua, fd, &s_id) |
0 и актуальный handle, либо -1. При допустимых аргументах и отсутствии регистрации записывает NULL; при неверных аргументах выход не меняет. |
Handle кодирует индекс без поколения и сохраняется при расширении массива. После удаления его нужно обнулить:
новая регистрация может получить то же числовое значение. Поколения защищают внутреннюю обработку событий,
но не делают безопасными вызовы API со старым handle. Строка name должна жить до удаления регистрации.
Межпоточное взаимодействие
| Функция | Описание |
|---|---|
uasync_post(ua, cb, arg) |
Потокобезопасно. Выделить и опубликовать задачу для потока экземпляра, попытаться разбудить его. При OOM задача не публикуется, ошибка логируется; статуса возврата нет. |
uasync_post_reserved(ua, task) |
Потокобезопасно. Передать заранее выделенный через u_malloc task с заполненными callback/arg без новых аллокаций. ua и task обязательны. Владение task переходит uasync. |
uasync_cancel_post(ua, task) |
В потоке владельца вне callbacks после завершения производителя: ERR_OK при удалении и освобождении ожидающего task, ERR_FAIL, если он не найден. ua и task обязательны. |
uasync_memsync(ua) |
Lock/unlock posted_lock живого экземпляра. Не ждёт исполнения задач; публикация через post уже использует этот мьютекс. |
uasync_wakeup(ua) |
0 при успешной записи, -1 при ошибке/неинициализированном wakeup. POSIX: запись uint64_t(1) в eventfd/pipe; Windows: send одного байта в UDP-сокет. Экземпляр должен оставаться живым. |
uasync_get_wakeup_fd(ua) |
Заимствованный endpoint записи, либо -1 при NULL/неинициализированном wakeup. Linux: eventfd, тот же fd для чтения и записи (писать uint64_t); прочие POSIX: pipe; Windows: UDP-сокет. Самостоятельно не закрывать. |
Время
| Функция | Описание |
|---|---|
get_time_tb() |
Время в единицах 0.1 мс: Linux CLOCK_BOOTTIME (включает suspend), остальные POSIX CLOCK_MONOTONIC, Windows QueryPerformanceCounter. Изменение календарных часов на шкалу не влияет. |
get_time_us() |
Монотонное время в микросекундах (для burst-измерений производительности). |
Диагностика
| Функция | Описание |
|---|---|
uasync_get_stats(ua, ...) |
Получить счётчики аллокаций/освобождений таймеров и сокетов. |
uasync_get_load(ua, &load) |
Актуализировать по сроку 100 мс и скопировать снимок CPU потока: текущий процент, среднее за секунду, время замера, длительность интервала и окна, valid. Только в потоке uasync. |
uasync_print_resources(ua, prefix) |
Вывести в лог все активные таймеры (имя, оставшееся время) и сокеты. Вызывается также в uasync_destroy перед очисткой. |
CPU измеряется с первого uasync_poll или uasync_get_load: по сроку 100 мс читается CPU-время текущего потока
(CLOCK_THREAD_CPUTIME_ID на POSIX, user+kernel GetThreadTimes на Windows). 100% означает одно полностью занятое ядро.
Учитывается вся работа этого потока между замерами, включая код между вызовами poll; CPU других потоков не учитывается.
Проценты вычисляются по фактической длительности интервала: 100 * cpu_ns / (elapsed_us * 1000).
Реальное время берётся из get_time_us (Linux: CLOCK_BOOTTIME, включая suspend; Windows: QPC).
Сроки измерения идут по сетке 100 мс без накопления дрейфа. Проверка выполняется при работе цикла и при запросе нагрузки.
Измеритель не создаёт таймеров, не ограничивает ожидание событий и не будит спящий поток.
После непрерывного ожидания не менее секунды весь старый интервал нагрузки вытеснен: текущий процент и среднее равны 0%.
Это фиксируется сразу после выхода из ожидания, до callbacks; CPU последующих обработчиков учитывается с новой точки отсчёта.
История хранится внутри UASYNC, без аллокаций. Частые запросы не создают лишних отсчётов.
Среднее ограничено последней секундой; вклад пересекающего границу интервала оценивается пропорционально перекрытию.
До первого замера valid=0, затем окно растёт до секунды. Долгий callback задерживает публикацию;
пропущенные отсчёты не восстанавливаются, текущий процент относится ко всему фактическому интервалу.
При большом перерыве вне ожидания среднее внутри этого интервала является оценкой по его общей загрузке.
Снимок содержит sample_time_us, чтобы потребитель мог показать возраст данных.
Ошибка CPU-часов логируется и отключает измеритель (valid=0), сохраняя работу цикла событий.
В sys=debug раз в секунду выводятся проценты, длительность интервала/окна и опоздание;
пропуск полного периода из-за долгой обработки выводится как WARN. Статус чата и headless-команда status показывают CPU и среднее.
GUI показывает округлённое секундное среднее 0–100% поверх полосы с градиентом зелёный → жёлтый → красный:
desktop — рядом с заголовком каналов, Android — рядом с версией в списке групп.
Запрос chat_core_collect_cpu_load_trampoline возвращает короткое событие CHAT_EVT_CPU_LOAD (valid:1, average_percent:1).
GUI опрашивает ядро каждые 333 мс, не более одного незавершённого запроса. Android опрашивает только пока экран списка групп RESUMED;
при паузе или уходе с экрана coroutine отменяется, фоновых запросов и таймеров ядра для индикатора нет.
Внутренние структуры
struct timeout_node— узел таймера: name, arg, callback, expiration_ms, heap_index. Используется и для таймеров в heap, и для FIFO-очереди immediate (через полеnext). Выделяется изtimeout_pool.struct socket_node— узел сокета: fd/sock, тип (FD/SOCK), колбэки, user_data, флаги active/enable_read/enable_write, gen (защита от stale epoll-событий после переиспользования fd).struct socket_array— массив сокетов: O(1) доступ по fd черезfd_to_index[], обход активных черезactive_indices[], динамическое расширение.struct posted_task— задача из другого потока: callback + arg + next, защищенаposted_lock.
Таймеры используют шкалу get_time_us; правила округления абсолютного срока и порядок обработки
call_soon описаны в разделах API выше. Сокеты и posted tasks обрабатываются после ожидания перед heap таймеров;
ошибка ожидания может завершить проход раньше. Установка stop не прерывает эти обработчики.
Для fallback poll массив дескрипторов резервируется при создании экземпляра и до регистрации нового сокета. Отказ памяти возвращает NULL из uasync_create / uasync_add_socket*; существующие регистрации сохраняются. Перестроение массива в uasync_poll не выделяет память. Расширение из callback сохраняет события текущего прохода, новый сокет опрашивается со следующего прохода.