Última atualização:2026-10-12
Princípio do event loop do libev¶
Outline
O libev é uma biblioteca de notificação de eventos eficiente e multiplataforma, usada principalmente para implementar mecanismos de eventos multiplexados (como select, poll, epoll etc.). Seu objetivo de projeto é fornecer um mecanismo de laço de eventos simples e rápido, especialmente adequado para cenários que exigem alta concorrência e baixa latência, como servidores Web de alta performance, proxies de rede e outras aplicações intensivas de IO. A quantidade de código é bem pequena e ela é usada em ambientes de produção; a leitura é muito recomendada. Nesta seção vamos falar dos seus princípios de implementação e do seu uso.
1.Estrutura básica¶
O núcleo do libev é o laço de eventos (event loop), que serve de interface entre a aplicação e o sistema operacional, usado para monitorar e despachar diferentes tipos de eventos. A arquitetura do libev é bastante enxuta e contém principalmente as seguintes partes:
1.1 Handles de evento (ev_watcher)¶
O libev gerencia e monitora os vários eventos por meio de handles de evento (watchers). Cada tipo de evento (eventos de IO, eventos de timer etc.) tem um tipo de watcher correspondente. Os handles de evento mais comuns incluem:
- ev_io: usado para monitorar eventos de leitura/escrita em descritores de arquivo.
- ev_timer: usado para eventos de timer, com suporte a timers de uma vez e repetitivos.
- ev_signal: usado para capturar eventos de sinal (como
SIGINT,SIGTERMetc.).
Todos os acima podem ser chamados de subclasses de ev_watcher:
typedef struct ev_watcher
{
int active;
int pending;
int priority;
void *data;
void (*cb)(struct ev_loop *loop, struct ev_watcher *w, int revents);
} ev_watcher;
ev_io stdin_watcher;
ev_io_init(&stdin_watcher, stdin_cb, /*STDIN_FILENO*/ 0, EV_READ);
ev_io_start(loop, &stdin_watcher);
do
{
do
{
((ev_watcher *)(void *)((&stdin_watcher)))->active = ((ev_watcher *)(void *)((&stdin_watcher)))->pending = 0;
((ev_watcher *)(void *)(((&stdin_watcher))))->priority = (0);
((((&stdin_watcher)))->cb = ((stdin_cb)), memmove(&((ev_watcher *)(((&stdin_watcher))))->cb, &(((&stdin_watcher)))->cb, sizeof((((&stdin_watcher)))->cb)));
} while (0);
do
{
((&stdin_watcher))->fd = ((0));
((&stdin_watcher))->events = ((EV_READ)) | EV__IOFDSET;
} while (0);
} while (0);
- Para um watcher já active, retorna diretamente
- Se ainda não estiver ativo, define active e priority
- Usa inserção no início para adicioná-lo à lista ligada do fd correspondente em anfds, e define o watcher atual como head
wlist_add(&((loop)->anfds)[fd].head, (WL)w);
static __inline__ void
wlist_add(WL *head, WL elem)
{
elem->next = *head;
*head = elem;
}
1.2 Multiplexação de IO¶
O libev dá suporte a epoll, poll, select etc.; dentro do loop há 2 funções callback:
void (*backend_modify)(struct ev_loop *loop, int fd, int oev, int nev);
void (*backend_poll)(struct ev_loop *loop, ev_tstamp timeout);
epoll_modify é o epoll_ctl que todos costumam usar: quando o evento muda, usa EPOLL_CTL_MOD; caso contrário, usa EPOLL_CTL_ADD.
A lógica de execução de epoll_poll é a seguinte:- epoll_wait obtém a lista de eventos prontos (loop->epoll_events)
- Para os eventos prontos, executa fd_event, colocando-os no array pendings do loop, no nível de prioridade correspondente.
static __inline__ void
fd_event(struct ev_loop *loop, int fd, int revents)
{
ANFD *anfd = ((loop)->anfds) + fd;
if (__builtin_expect((!!(!anfd->reify)), (1)))
fd_event_nocheck(loop, fd, revents);
}
static __inline__ void
fd_event_nocheck(struct ev_loop *loop, int fd, int revents)
{
ANFD *anfd = ((loop)->anfds) + fd;
ev_io *w;
for (w = (ev_io *)anfd->head; w; w = (ev_io *)((WL)w)->next)
{
int ev = w->events & revents;
if (ev)
ev_feed_event(loop, (W)w, ev);
}
}
// 省略部分代码
void __attribute__((__noinline__))
ev_feed_event(struct ev_loop *loop, void *w, int revents)
{
w_->pending = ++((loop)->pendingcnt)[pri];
((loop)->pendings)[pri][w_->pending - 1].w = w_;
((loop)->pendings)[pri][w_->pending - 1].events = revents;
}
1.3 Timer¶
A implementação interna do Timer usa heap binário e heap quaternário; o heap quaternário é usado porque tem melhor eficiência de cache. Sua implementação é a seguinte:
#define DHEAP 4
#define HEAP0 (DHEAP - 1) /* index of first element in heap */
#define HPARENT(k) ((((k) - HEAP0 - 1) / DHEAP) + HEAP0)
#define UPHEAP_DONE(p,k) ((p) == (k))
1.4 Laço de eventos (ev_loop)¶
O uso típico é o seguinte:
EV_DEFAULT chama a função ev_default_loop, que inicializa uma estrutura ev_loop; dentro dela, loop_init inicializa a estrutura do loop, por exemplo: escolher entre epoll, poll ou select.O ponto mais essencial: o epoll_init acontece na criação do loop acima.
O ev_run é bem complexo e faz principalmente algumas coisas:
- fd_reify
Percorre o array fdchanges mencionado antes, retira do anfds a lista ligada correspondente ao fd, percorre todas as listas ligadas de eventos para obter todos os eventos e verifica se o evento mais recente é igual ao evento antigo anterior (events); se for diferente, chama epoll_modify (ou adiciona o evento pela primeira vez).
- backend_pollVeja a implementação de backend_poll anterior: ele obtém os eventos prontos e os coloca no array pendings, ou seja, no final da fila de prioridade correspondente.
-
timer_reify: se o evento não expirou, nada é feito; se expirou, executa a lógica abaixo:
-
timer sem repeat definido
- chama ev_timer_stop para removê-lo
-
timer com repeat definido
Por exemplo: `ev_timer_init(&timer_watcher, timer_cb, 2.0, 3.0);``
`ev_timerdefineatcomo o tempo atual mais 2 segundos (ou seja, o timer disparará em 2 segundos) erepeaté definido como 3 segundos; assim, o timer dispara primeiro após 2 segundos e, a partir daí, a cada 3 segundos. -
Invoke_cb
Invoke_cb é por padrão ev_invoke_pending: retira o evento da fila de prioridade correspondente e chama a função callback para tratá-lo.
ANPENDING *p = ((loop)->pendings)[((loop)->pendingpri)] + --((loop)->pendingcnt)[((loop)->pendingpri)];
(p->w)->cb(loop, (p->w), (p->events));
2.Como usar?¶
Compilação do projeto:3.Exercício¶
Por fim, deixo um exercício: você consegue escrever com o libev um exemplo como este?
Quando inserimos dados, o evento de stdio retorna ready; se a entrada ultrapassar o tempo limite, retorna timeout. Efeito da demonstração:
Tópicos relacionados¶
- Guia de C++ Concurrency in Action | C++那些事
- Depuração de Deadlocks em C++ | C++那些事
- Vetorização SIMD em C++ | C++那些事
- STL Paralela do C++17 | C++那些事
- False Sharing e Linhas de Cache em C++ | C++那些事
- Depuração All-Stop multi-thread com GDB | C++那些事
- Detecção de Erros de Concorrência com Helgrind | C++那些事
- Implementação interna de std::thread em C++ | C++那些事
- C++ std::thread e jthread | C++那些事
- Introdução a multi-thread em C++ | C++那些事