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.
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ões | Laço de eventos | Thread por conexão |
|---|---|---|
| 400 | 33 ms · 1 thread · +0 MB | 43 ms · 400 threads · +14 MB |
| 1000 | 73 ms · 1 thread · +1 MB | 83 ms · 1000 threads · +36 MB |
| 2000 | 151 ms · 1 thread · +0 MB | 161 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.
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 50Entrada e saída#
| Símbolo | O 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 de | Use |
|---|---|
| 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 tempo | este 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 template | Kiln — ele é thread por pedido, e para a maioria dos casos isso basta |