u_async — Центральный цикл событий (event loop)
1. Назначение
u_async — центральный планировщик проекта uTun. Каждый поток, которому нужна асинхронная обработка сокетов, таймеров и отложенных задач, создаёт свой экземпляр struct UASYNC и запускает uasync_mainloop() / uasync_poll(). Модуль объединяет:
- Сокеты — мониторинг fd/socket на чтение/запись/ошибки через Linux epoll (предпочтительно) с fallback на poll/select.
- Таймеры — однократные таймеры с точностью 0.1 мс, реализованные на базе
timeout_heap (min-heap) и memory_pool.
- Немедленное выполнение — FIFO-очередь
uasync_call_soon для отложенного запуска callback в ближайшей итерации цикла.
- Межпоточную связь —
uasync_post позволяет другому потоку безопасно запланировать callback в главном потоке (wakeup через pipe/UDP-сокет).
На Linux используется epoll, на FreeBSD/Windows — poll/select. Платформенная абстракция прозрачна для вызывающего кода.
2. Как пользоваться
Правила
- Один UASYNC на поток. Нельзя создать несколько экземпляров и вызывать их из разных потоков.
- Никаких sleep/usleep. Если поток заблокирован на sleep, цикл событий стоит. Все ожидания — только через таймеры
uasync_set_timeout.
- Закрытие из callback. Можно вызывать
uasync_destroy или uasync_remove_socket прямо из callback сокета/таймера. Модуль использует локальные копии указателей перед вызовом callback и генерационные счётчики (gen) для защиты от stale epoll-событий.
- Указатель
user_arg передаётся во все callback и позволяет передать контекстную структуру (например, UTUN_INSTANCE).
Типовой сценарий
// 1. Создать uasync
struct UASYNC* ua = uasync_create();
if (!ua) { /* ошибка */ }
// 2. Зарегистрировать сокет
void* sock_id = uasync_add_socket(ua, fd, my_read_cb, NULL, NULL, my_ctx);
// или для socket_t:
void* sock_id = uasync_add_socket_t(ua, sock, my_read_cb_sock, my_write_cb_sock, NULL, 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. Отложенное выполнение в следующей итерации
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. Или один шаг с таймаутом (timebase, -1 = бесконечно)
uasync_poll(ua, 500); // ждать до 50 мс или первого события
// 9. Завершение
uasync_destroy(ua, 1); // close_fds=1 — закрыть все сокеты
Таймеры: правильный паттерн
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;
// работаем в главном потоке — можно вызывать любые функции uasync, трогать сокеты и т.д.
uasync_set_timeout(u->ua, 10, u, handle_work, "work");
}
void thread_func(UTUN_INSTANCE* u) {
// …
uasync_post(u->ua, notify_main, u);
}
// Для гарантии видимости памяти из другого потока перед uasync_post:
uasync_memsync(u->ua);
Получение времени
uint64_t now_tb = get_time_tb(); // timebase 0.1 мс (монотонные часы)
uint64_t now_us = get_time_us(); // микросекунды (для burst-измерений)
3. API
Жизненный цикл
| Функция |
Описание |
uasync_create() |
Создать экземпляр: аллоцирует структуру, socket_array, timeout_heap, memory_pool, epoll/poll, wakeup pipe/сокет. |
uasync_destroy(ua, close_fds) |
Уничтожить экземпляр. При close_fds=1 закрывает все зарегистрированные fd. Перед уничтожением выводит диагностику ресурсов и проверяет на утечки (abort при несовпадении аллокаций). |
uasync_stop(ua) |
Установить флаг stop = 1 — на следующей итерации uasync_mainloop выйдет. |
uasync_mainloop(ua) |
Бесконечный цикл while(!stop) uasync_poll(ua, -1). |
uasync_poll(ua, timeout_tb) |
Одна итерация: обработать сокеты + таймеры. timeout_tb = максимальное ожидание в timebase; -1 = ждать следующего таймера (или бесконечно, если нет таймеров). |
Таймеры (timebase = 0.1 мс)
| Функция |
Описание |
uasync_set_timeout(ua, timeout_tb, arg, cb, name) |
Запланировать однократный таймер. Возвращает дескриптор void* для отмены. name — до 15 символов, используется в логах. |
uasync_cancel_timeout(ua, t_id) |
Отменить таймер по дескриптору. После отмены дескриптор нужно обнулить — повторный cancel даст ошибку. |
Немедленное выполнение (FIFO)
| Функция |
Описание |
uasync_call_soon(ua, arg, cb) |
Запланировать callback на ближайшую итерацию цикла. Используется, когда нужно отложить выполнение на «сразу после текущих событий». Узел выделяется из того же timeout_pool. |
uasync_call_soon_cancel(ua, t_id) |
Отменить — просто зануляет callback, узел будет освобождён при обработке очереди (O(1)). |
Сокеты
| Функция |
Описание |
uasync_add_socket(ua, fd, r_cb, w_cb, e_cb, arg) |
Добавить fd (pipe, file). r_cb/w_cb/e_cb могут быть NULL. Возвращает дескриптор void*. |
uasync_add_socket_t(ua, sock, r_cb, w_cb, e_cb, arg) |
Добавить socket_t (кросс-платформенный сокет). |
uasync_remove_socket(ua, s_id) |
Удалить сокет по дескриптору. Помечает слот неактивным, не освобождает индексную ячейку (защита от stale epoll-событий через gen). |
uasync_remove_socket_t(ua, sock) |
Удалить по значению socket_t. |
uasync_set_socket_read(ua, s_id, enable) |
Динамически включить/отключить мониторинг чтения (EPOLL_CTL_MOD). |
uasync_set_socket_write(ua, s_id, enable) |
Динамически включить/отключить мониторинг записи. |
uasync_lookup_socket(ua, fd, &s_id) |
Найти дескриптор сокета по fd (возвращает актуальный указатель даже после realloc). |
Межпоточное взаимодействие
| Функция |
Описание |
uasync_post(ua, cb, arg) |
Потокобезопасно. Запланировать callback в главном потоке. Выделяет posted_task, добавляет в связный список под мьютексом, будит главный поток через uasync_wakeup. |
uasync_memsync(ua) |
Потокобезопасно. Барьер памяти (lock/unlock posted_lock) для гарантии видимости данных, записанных из другого потока перед uasync_post. |
uasync_wakeup(ua) |
Разбудить poll/epoll_wait записью байта в wakeup pipe (POSIX) или send в UDP-сокет (Windows). Можно вызывать из обработчика сигналов. |
uasync_get_wakeup_fd(ua) |
Получить write-fd wakeup pipe для использования в signalfd или кастомных механизмах. |
Время
| Функция |
Описание |
get_time_tb() |
Монотонное время в timebase (0.1 мс). clock_gettime(CLOCK_MONOTONIC) на POSIX, QueryPerformanceCounter на Windows. |
get_time_us() |
Монотонное время в микросекундах (для burst-измерений производительности). |
Диагностика
| Функция |
Описание |
uasync_get_stats(ua, ...) |
Получить счётчики аллокаций/освобождений таймеров и сокетов. |
uasync_print_resources(ua, prefix) |
Вывести в лог все активные таймеры (имя, оставшееся время) и сокеты. Вызывается также в uasync_destroy перед очисткой. |
Внутренние структуры
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.