Pular para o conteúdo

Especificação §12

IO e async

language-design.md §12 · 76 linhas · 2 min de leitura

Inspirado no Zig 0.17: async é desacoplado do sistema de tipos. O desenvolvedor escreve código linear, sem se preocupar se a operação é blocking ou non-blocking. A interface IO resolve isso internamente.

Sem “function coloring”: uma função que faz IO não força seu caller a ser marcado como async. O scheduler cuida da suspensão e da retomada de processos de forma transparente.

Em linguagens com function coloring (JavaScript, Python, Rust/Tokio), async infecta o call stack:

// Rust/JS: async vaza para cima infinitamente
async fn read_file() -> String { ... }
async fn process() -> Data { read_file().await } // forçado
async fn handle() -> Response { process().await } // forçado
async fn main() { handle().await } // forçado

Aqui, nenhuma função é marcada. O scheduler suspende o processo quando IO bloqueia e o retoma quando o dado chega:

// Parece síncrono, é não-bloqueante por baixo
fn read_file(path: string) -> Result[string, error{NotFound}] {
return io.read(path) // scheduler suspende este processo aqui se necessário
}
fn process(path: string) -> Result[Data, error{...}] {
content := read_file(path) catch |e| { return e }
return parse(content)
}
fn handle(req: Request) -> Response {
data := process(req.path) catch |e| {
return Response.error(e)
}
return Response.ok(data)
}

io.read é uma chamada normal de função. Internamente, ela é implementada via a interface IO do runtime, que pode usar epoll, io_uring ou kqueue, conforme o OS. O processo atual é suspenso pelo scheduler enquanto aguarda, liberando o core para outros processos. Quando o dado chega, o scheduler retoma de onde parou.

Quando você quer múltiplas operações de IO em paralelo, usa processos, não async/await nem Futures:

// IO paralelo: cada processo faz sua leitura independentemente
chan_a: Channel[->string] = ...
chan_b: Channel[->string] = ...
spawn fetch_url("https://api-a.com", chan_a)
spawn fetch_url("https://api-b.com", chan_b)
// Espera os dois, bloqueia só quando necessário
result_a := -> chan_a catch |e| { return }
result_b := -> chan_b catch |e| { return }

Ou, se quiser o primeiro que chegar, com match sem sujeito (a forma “select”; seção 4):

match {
data := -> chan_a => handle(data)
data := -> chan_b => handle(data)
} timeout(5s) {
return error.Timeout
}
  • Uma chamada io.* nunca bloqueia o scheduler, só o processo que a fez (epoll ou io_uring por baixo). Uma chamada FFI bloqueante (seção 17) é outra história: o work-stealing a absorve num pool, e o determinístico exige que você a isole. Ver “FFI bloqueante e o scheduler” (seção 3).
  • O processo é suspenso cooperativamente no ponto exato do io.*; entre instruções, o scheduler ainda pode preemptá-lo (seção 3).
  • Zero overhead de alocação de Future ou Task, pois o processo já é a unidade de suspensão.