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 desendbloquear.c.send(i * i)bloqueia se o buffer estiver cheio.c.recv()bloqueia se o buffer estiver vazio e retorna o valor diretamente — umT, não um wrapper. Em um canal fechado e drenado, retorna umTcom 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-chaveloopem Glide — usewhile letaqui, ouwhile 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:
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:
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.