Pular para o conteúdo

O laço de eventos

Uma thread dormindo no seletor do sistema em vez de uma thread por conexão — com o número medido: 2000 conexões, 1 thread, +0 MB.

O `async/await` desta linguagem é uma thread por tarefa. Serve para o que foi feito — sobrepor entrada e saída — e não escala: mil conexões simultâneas são mil threads do sistema. O Kiln atende um pedido por thread pelo mesmo motivo.

O outro modelo é o reator: uma thread que dorme num seletor do sistema — epoll no Linux, kqueue no macOS e no BSD, select no Windows — e acorda quando algum descritor tem trabalho.

dataforge
adopt Arcane.Laco as L

laco := L.novo()
ordem := []

L.agendar(laco, lambda => ordem.append("agora"))
L.apos(laco, 30, lambda => ordem.append("depois"))
L.apos(laco, 10, lambda => ordem.append("antes"))
L.rodar(laco)

assert ordem is ["agora", "antes", "depois"]
assert L.mecanismo(laco) in ["epoll", "kqueue", "select", "poll"]

Quem foi agendado depois mas vence antes roda antes: a fila de prazos é um heap, não uma lista percorrida.

O número#

Um servidor de linha, uma requisição por conexão, medido nesta máquina contra o mesmo servidor com uma thread por conexão:

ConexõesLaço de eventosThread por conexão
40033 ms · 1 thread · +0 MB43 ms · 400 threads · +14 MB
100073 ms · 1 thread · +1 MB83 ms · 1000 threads · +36 MB
2000151 ms · 1 thread · +0 MB161 ms · 2000 threads · +36 MB

Ele não gira em vão#

É a diferença entre um reator e uma espera ocupada, e ela é testada: com um único temporizador a 200 ms, o laço dá menos de 50 voltas. Um laço de espera ocupada daria milhões, e queimaria um núcleo sem fazer nada.

dataforge
adopt Arcane.Laco as L

laco := L.novo()
L.apos(laco, 60, lambda => void)
L.rodar(laco)

// dormiu no seletor em vez de girar
assert L.estatisticas(laco)["voltas"] smaller 50

Entrada e saída#

SímboloO que faz
L.novo(teto?)um laço; com teto, a fila de prontas recusa quando enche
L.rodar(laco)gira até acabar o trabalho, ou até alguém chamar parar
L.agendar(laco, acao)põe na fila; devolve no quando o teto recusa
L.apos(laco, ms, acao)uma vez, depois do prazo
L.a_cada(laco, ms, acao)repete até ser cancelado
L.quando_ler(laco, soquete, acao)chama quando houver o que ler
L.quando_escrever(…) · L.esquecer(…)o outro lado, e a saída do laço
L.executar(laco, trabalho, depois)manda o que bloqueia para o pool
L.cancelar(t) · L.cancelada(t)vale para tarefa e para fibra
L.mecanismo(laco)epoll, kqueue ou select — o do sistema
L.estatisticas(laco) · L.falhas(laco)voltas, tarefas, prazos, E/S, erros

Quando usar qual#

Precisa deUse
sobrepor duas ou três chamadas de rede`async`/`await` — mais simples, e o custo não aparece
milhares de conexões abertas ao mesmo tempoeste módulo
usar mais de um núcleo de CPU`P.map_processos` — o laço é uma thread só, e o GIL continua no caminho
servir HTTP com rota e templateKiln — ele é thread por pedido, e para a maioria dos casos isso basta