Capítulo 07 8 min de leitura

Concorrência

Se um programa espera uma leitura de rede, depois uma escrita em disco, depois outra leitura de rede — ele passa a maior parte do tempo bloqueado no relógio de outra coisa. A solução é fazer outro trabalho útil durante essas esperas. Há várias maneiras de fazer isso; Glide escolhe uma forma específica inspirada em Go.

As duas maneiras que o Glide escalona trabalho

Você vai conhecer duas palavras-chave. Elas parecem quase idênticas, mas fazem coisas bem diferentes por baixo dos panos.

  • `spawn` — inicia uma corrotina (também chamada de "go-routine" no Go). Barato: uma corrotina custa ~192 bytes de memória em Glide. O runtime consegue escalonar milhões delas em apenas algumas threads de SO (é isso que "M:N" significa — M corrotinas em N threads). Use para a maioria das situações.
  • `spawn_thread` — inicia uma thread de SO de verdade. Caro (~1 MB de pilha cada), mas cada uma é verdadeiramente independente. Use quando precisar chamar algo que bloqueia uma thread de SO (uma biblioteca C síncrona, uma syscall bloqueante que o runtime não consegue interceptar).

No dia a dia com Glide, você vai usar `spawn` quase exclusivamente.

fn worker(id: i32) {
    println!("worker", id, "starting");
    // ... faz alguma coisa
    println!("worker", id, "done");
}

fn main() -> i32 {
    spawn worker(1);
    spawn worker(2);
    spawn worker(3);
    // espera por elas de alguma forma — veja canais abaixo
    return 0;
}

Canais: como as corrotinas se comunicam

As corrotinas não compartilham variáveis por padrão — elas se comunicam através de canais. Um canal é um pipe tipado: em uma extremidade você envia com .send(value), e na outra recebe com .recv().

fn producer(c: chan<i32>) {
    for i in 0..5 {
        c.send(i * i);
    }
    c.close();
}

fn main() -> i32 {
    let c: chan<i32> = make_chan(2);     // com buffer: comporta até 2 valores pendentes
    spawn producer(c);

    while let v = c.recv() {              // drena o canal até ele ser fechado
        println!("got:", v);
    }
    return 0;
}

O que está acontecendo:

  • make_chan(2) cria um canal com buffer capaz de armazenar 2 valores antes de send bloquear.
  • c.send(i * i) bloqueia se o buffer estiver cheio.
  • c.recv() bloqueia se o buffer estiver vazio e retorna o valor diretamente — um T, não um wrapper. Em um canal fechado e drenado, retorna um T com valor zero.
  • while let v = c.recv() { ... } é o idioma para "drenar até fechar": extrai valores e para limpo quando o canal é fechado e vazio. (Não existe a palavra-chave loop em Glide — use while let aqui, ou while true { ... } para um loop incondicional.)
  • c.close() sinaliza que "não vêm mais valores".

Canais sem buffer

Passe 0 para make_chan para ter um canal síncrono — emissor e receptor precisam se encontrar no mesmo ponto no tempo. Útil para handshakes:

let ready: chan<i32> = make_chan(0);
spawn function_that_signals_when_ready(ready);
ready.recv();          // bloqueia até que a corrotina filha envie algo
println!("the worker said it's ready");

select!: aguardando múltiplos canais

Quando uma corrotina quer esperar por qualquer um de vários eventos que disparar primeiro, use select!. Mesma forma que o select do Go ou o receive do Erlang.

import stdlib::time::*;

fn main() -> i32 {
    let a: chan<string> = make_chan(0);
    let b: chan<string> = make_chan(0);
    let timeout: chan<i64> = after(Duration::from_millis(2000));   // dispara após 2s

    spawn slow_query(a);
    spawn slow_query(b);

    select! {
        msg = a.recv() => { println!("a said:", msg); }
        msg = b.recv() => { println!("b said:", msg); }
        _   = timeout.recv() => { println!("timed out"); }
    }
    return 0;
}

Cada braço tem a forma binding = chan.recv() => { ... }. O recv() entrega o valor diretamente, então você usa msg (não msg.val) dentro do braço; use _ quando só importa que o canal disparou. O primeiro braço cujo canal estiver pronto vence; os demais são abandonados. Se vários estiverem prontos ao mesmo tempo, select! escolhe um (atualmente na ordem dos braços; versões futuras podem aleatorizar).

Um pool de workers

Juntando tudo: inicie N corrotinas worker, alimente-as com jobs por um canal e colete os resultados por outro canal. Padrão clássico, ~25 linhas:

workers.glide
fn worker(id: i32, jobs: chan<i32>, results: chan<i32>) {
    while let j = jobs.recv() {            // drena até jobs ser fechado
        results.send(j * 2);               // finge que o trabalho é `value * 2`
    }
    println!("worker", id, "exiting");
}

fn main() -> i32 {
    let jobs:    chan<i32> = make_chan(10);
    let results: chan<i32> = make_chan(10);

    for w in 1..=3 { spawn worker(w, jobs, results); }

    // enfileira 8 jobs
    for j in 1..=8 { jobs.send(j); }
    jobs.close();

    // coleta 8 resultados
    for _ in 0..8 {
        let r: i32 = results.recv();
        println!("result:", r);
    }
    return 0;
}

Três workers compartilham a fila. O primeiro que estiver livre pega o próximo job. Baseado em canais, sem memória compartilhada, sem locks.

Quando você realmente precisa de estado compartilhado

Às vezes um dado genuinamente precisa ser acessado por várias corrotinas — um contador, um cache, um registro. Use stdlib::sync::Mutex<T>:

import stdlib::sync::*;

fn increment(m: *Mutex<i32>) {
    m.lock();
    m.set(m.get() + 1);   // lê + escreve o valor protegido
    m.unlock();
}

fn main() -> i32 {
    let m: *Mutex<i32> = Mutex::new(0);
    increment(m);
    increment(m);
    println!(m.get());    // 2
    return 0;
}

m.lock() bloqueia até o mutex estar livre; m.get() / m.set(v) leem e escrevem o valor protegido; m.unlock() o libera. Lock e unlock vêm em pares — todo lock() precisa de um unlock() correspondente em todo caminho de saída. Se preferir não controlar isso manualmente, m.with(fn) executa uma função enquanto mantém o lock e o libera ao terminar.

Quando usar spawn_thread

Se você estiver chamando algo que bloqueia a thread de SO — uma biblioteca C síncrona que não coopera com o runtime — você precisa de uma thread de SO real para não congelar o escalonador de corrotinas:

spawn_thread call_into_blocking_c_library();

No código Glide comum (I/O da biblioteca padrão, HTTP, canais, sleep), o runtime intercepta as chamadas bloqueantes e suspende a corrotina em vez da thread. Por isso você raramente precisa de spawn_thread.

Um exemplo real: um pequeno servidor HTTP de eco

Juntando spawn + canais + I/O. Este é um servidor que funciona de verdade:

echo_server.glide
import stdlib::net::listener::*;

fn handle(c: *Conn) {
    let buf: *u8 = malloc(4096) as *u8;
    defer free(buf as *void);
    let n: i32 = c.read(buf as *void, 4096);
    if n > 0 { c.write(buf as *void, n); }
    c.close();
}

fn main() -> i32 {
    let l: *Listener = Listener::bind(8080);
    if !l.ok() { return 1; }
    println!("echo server on :8080");
    while true {
        let c: *Conn = l.accept();
        spawn handle(c);     // cada conexão ganha sua própria corrotina
    }
    return 0;
}

Cada conexão recebida vira sua própria corrotina — o runtime consegue lidar com centenas de milhares delas de forma concorrente em apenas algumas threads de SO.

Onde ir a seguir

Você já sabe iniciar corrotinas, conectá-las com canais e encerrar com prazo definido usando select!. O último capítulo — Módulos e pacotes — mostra como organizar múltiplos arquivos em um projeto e incorporar código externo.