Sincronização
Primitivas de concorrência para compartilhar estado entre corrotinas iniciadas com spawn: Mutex<T> para exclusão mútua sobre um valor tipado, Atomic para um i64 sem lock, e WaitGroup para aguardar um conjunto de workers terminar. As três são wrappers alocados no heap que pertencem ao chamador — pareie cada new() com um defer .free().
Import
import stdlib::sync::*;
Visão geral
| Tipo | Finalidade | Construção | Liberação |
|---|---|---|---|
Mutex<T> |
Valor mutável protegido de qualquer tipo T |
Mutex::new(initial) |
m.free() |
Atomic |
i64 compartilhado sem lock (contadores, flags, números de sequência) |
Atomic::new(initial) |
a.free() |
WaitGroup |
Bloquear até N corrotinas terminarem | WaitGroup::new() |
wg.free() |
Essas primitivas são construídas sobre pthread_mutex / pthread_cond (a mesma camada que os canais do runtime usam) e _Atomic do C11. Cada construtor retorna um ponteiro bruto (*Mutex<T>, *Atomic, *WaitGroup); o wrapper só é liberado quando você chama .free(), portanto o idioma padrão é:
let m: *Mutex<i32> = Mutex::new(0);
defer m.free();
Escolhendo uma primitiva
| Se você precisa… | Use | Por quê |
|---|---|---|
Um contador / flag / número de sequência compartilhado (i64) |
Atomic |
Sem lock; nenhuma seção crítica para esquecer de liberar. |
| Proteger um struct, um vetor ou um invariante de múltiplos passos | Mutex<T> |
Um único lock cobre toda uma transação de "ler, mutar, gravar". |
| Aguardar um conjunto fixo de workers iniciados com spawn terminarem | WaitGroup |
add antes, done em cada worker, wait no pai. |
| Init "somente o primeiro a chegar vence", de uso único | Atomic::cas |
Test-and-set atômico sem lock. |
Mutex<T>
Um mutex que protege um valor do tipo T. Use .with() para o padrão comum de ler-mutar-gravar, ou .lock() / .get() / .set() / .unlock() para controle explícito.
pub struct Mutex<T> {
inner: T,
handle: *void,
}
Ambos os campos são privados; acesse o valor por meio de get / set / with.
Catálogo de métodos
| Método | Assinatura | Descrição |
|---|---|---|
new |
Mutex::new(initial: T) -> *Mutex<T> |
Cria um mutex encapsulando initial; começa desbloqueado. |
lock |
fn lock(self: *Mutex<T>) |
Adquire o lock, bloqueando até que esteja disponível. |
unlock |
fn unlock(self: *Mutex<T>) |
Libera o lock. Chamar sem mantê-lo é comportamento indefinido. |
get |
fn get(self: *Mutex<T>) -> T |
Lê (uma cópia d)o valor interno. O chamador deve manter o lock. |
set |
fn set(self: *Mutex<T>, v: T) |
Grava o valor interno. O chamador deve manter o lock. |
with |
fn with<U>(self: *Mutex<T>, f: fn(*T) -> U) -> U |
Executa f(&inner) mantendo o lock; retorna o resultado de f. |
free |
fn free(self: *Mutex<T>) |
Destrói o mutex pthread e libera o wrapper. |
Nenhum desses métodos retorna !T/?T — não há operações falíveis. Os únicos modos de falha são erros de uso (lock/unlock desbalanceados, locking recursivo), que são comportamento indefinido em vez de erros retornados.
new
pub fn new(initial: T) -> *Mutex<T>
Aloca o wrapper no heap, armazena initial e inicializa um mutex pthread desbloqueado. O *Mutex<T> retornado é um ponteiro bruto de sua posse; libere-o com free().
lock / unlock / get / set
Adquira com lock(), mute por meio de get() / set(), depois libere com unlock(). Cada lock() deve ser combinado com exatamente um unlock().
import stdlib::sync::*;
fn main() -> i32 {
let m: *Mutex<i32> = Mutex::new(0);
defer m.free();
m.lock();
m.set(m.get() + 1);
m.unlock();
m.lock();
let v: i32 = m.get();
m.unlock();
println!("counter =", v);
return 0;
}
with
with<U>(f) adquire o lock, chama f com um ponteiro para o valor interno e libera o lock antes de retornar o resultado de f. Esta é a forma segura de fazer "ler, mutar, gravar" sem ter que gerenciar lock / unlock manualmente. O callback recebe *T, portanto pode ler e mutar o valor no lugar — e ao contrário de get(), a mutação afeta o valor protegido.
import stdlib::sync::*;
fn bump(p: *i32) -> i32 {
*p = *p + 1;
return *p;
}
fn main() -> i32 {
let m: *Mutex<i32> = Mutex::new(10);
defer m.free();
let after: i32 = m.with(bump); // adquire o lock, incrementa, libera o lock
println!("after =", after); // 11
return 0;
}
with funciona com qualquer tipo de carga útil, inclusive structs:
import stdlib::sync::*;
struct Stats { hits: i32, misses: i32 }
fn add_hit(p: *Stats) -> i32 {
p.hits = p.hits + 1;
return p.hits;
}
fn main() -> i32 {
let m: *Mutex<Stats> = Mutex::new(Stats{ hits: 0, misses: 0 });
defer m.free();
let h: i32 = m.with(add_hit);
println!("hits =", h);
return 0;
}
Exemplo prático: contador compartilhado entre corrotinas
O principal uso de um Mutex é proteger um valor que muitas corrotinas acessam ao mesmo tempo. Aqui, quatro workers chamam with(inc) 100 vezes cada; o WaitGroup faz o main aguardar todos eles antes de ler o total final.
import stdlib::sync::*;
fn inc(p: *i32) -> i32 {
*p = *p + 1;
return *p;
}
fn worker(m: *Mutex<i32>, wg: *WaitGroup) {
defer wg.done();
for let i: i32 = 0; i < 100; i++ {
m.with(inc);
}
}
fn main() -> i32 {
let m: *Mutex<i32> = Mutex::new(0);
defer m.free();
let wg: *WaitGroup = WaitGroup::new();
defer wg.free();
wg.add(4);
for let w: i32 = 0; w < 4; w++ {
spawn worker(m, wg);
}
wg.wait();
m.lock();
let total: i32 = m.get();
m.unlock();
println!("total =", total); // 400
return 0;
}
total = 400
Sem o mutex, a operação de ler-modificar-gravar *p = *p + 1 geraria uma corrida e o total ficaria abaixo de 400. (Para um contador simples de i64 como esse, um `Atomic` é mais leve — veja abaixo; o Mutex brilha quando o valor protegido é um struct ou a atualização abrange vários campos.)
Exemplo prático: protegendo um struct
with() e lock/get/set explícitos são intercambiáveis; with() é preferível porque não pode deixar um lock mantido vazar. A forma explícita é conveniente quando você precisa de um valor lido fora da seção crítica:
import stdlib::sync::*;
struct Account { balance: i32 }
fn deposit(p: *Account) -> i32 {
p.balance = p.balance + 50;
return p.balance;
}
fn main() -> i32 {
let m: *Mutex<Account> = Mutex::new(Account{ balance: 100 });
defer m.free();
let bal: i32 = m.with(deposit);
println!("balance =", bal); // 150
// lock/get/set explícito é equivalente:
m.lock();
let cur: Account = m.get();
m.set(Account{ balance: cur.balance - 30 });
m.unlock();
m.lock();
let after: Account = m.get();
m.unlock();
println!("after withdraw =", after.balance); // 120
return 0;
}
free
Libera o mutex pthread subjacente e desaloca o wrapper. O ponteiro fica pendente após isso, portanto sempre combine com defer.
let m: *Mutex<i32> = Mutex::new(0);
defer m.free();
Atomic
Um i64 sem lock. Use-o para contadores compartilhados, flags ou números de sequência — qualquer caso em que um Mutex<i32> dominaria o quadro de contenção.
pub struct Atomic {
handle: *void,
}
Todas as operações são sequencialmente consistentes (a ordem de memória mais forte e simples — leituras e gravações aparecem em uma única ordem global).
Catálogo de métodos
| Método | Assinatura | Descrição |
|---|---|---|
new |
Atomic::new(initial: i64) -> *Atomic |
Cria um atomic inicializado com initial. |
load |
fn load(self: *Atomic) -> i64 |
Carrega o valor atual. |
store |
fn store(self: *Atomic, v: i64) |
Armazena v. |
add |
fn add(self: *Atomic, delta: i64) -> i64 |
Soma delta, retorna o valor anterior. |
cas |
fn cas(self: *Atomic, expected: i64, desired: i64) -> bool |
Compare-and-swap; true se a troca ocorreu. |
free |
fn free(self: *Atomic) |
Libera o wrapper. |
Para subtrair ou decrementar, passe um delta negativo: a.add(0 - 1) (Glide não tem menos unário literal nessa posição; construa o negativo com 0 - n).
load / store / add
add retorna o valor antes da adição, o que o torna um gerador natural de IDs únicos / sementes de sequência (add(1) entrega a cada chamador uma contagem anterior distinta).
import stdlib::sync::*;
fn main() -> i32 {
let seq: *Atomic = Atomic::new(0);
defer seq.free();
let first: i64 = seq.add(1); // 0 (era 0, agora 1)
let second: i64 = seq.add(1); // 1 (era 1, agora 2)
let third: i64 = seq.add(1); // 2 (era 2, agora 3)
println!("tickets:", first, second, third);
println!("next free id =", seq.load()); // 3
return 0;
}
tickets: 0 1 2
next free id = 3
Exemplo prático: incremento atômico entre corrotinas
É aqui que Atomic se paga: oito corrotinas fazem 1000 incrementos cada sem nenhum lock, e o total é exato porque add é atômico.
import stdlib::sync::*;
fn bumper(a: *Atomic, wg: *WaitGroup) {
defer wg.done();
for let i: i32 = 0; i < 1000; i++ {
a.add(1);
}
}
fn main() -> i32 {
let a: *Atomic = Atomic::new(0);
defer a.free();
let wg: *WaitGroup = WaitGroup::new();
defer wg.free();
wg.add(8);
for let w: i32 = 0; w < 8; w++ {
spawn bumper(a, wg);
}
wg.wait();
let total: i64 = a.load();
println!("total =", total); // 8000
return 0;
}
total = 8000
cas
Compare-and-swap: se o valor atual for igual a expected, substitui pelo desired e retorna true; caso contrário, deixa inalterado e retorna false. Use-o para guards de uso único onde apenas o primeiro a chegar deve vencer.
import stdlib::sync::*;
fn try_init(flag: *Atomic, id: i32) {
if flag.cas(0, 1) {
println!("worker won init:", id);
} else {
println!("worker skipped:", id);
}
}
fn main() -> i32 {
let flag: *Atomic = Atomic::new(0);
defer flag.free();
try_init(flag, 1); // vence
try_init(flag, 2); // pula
try_init(flag, 3); // pula
println!("flag =", flag.load()); // 1
return 0;
}
worker won init: 1
worker skipped: 2
worker skipped: 3
flag = 1
WaitGroup
Um contador que você pode aguardar chegar a zero. Espelha o sync.WaitGroup do Go: add(n) antes de fazer spawn, done() em cada worker, wait() no pai.
pub struct WaitGroup {
handle: *void,
}
Internamente é um contador protegido por mutex + variável de condição: add incrementa o contador (fazendo broadcast quando chega a zero), done decrementa e wait bloqueia na condvar até o contador ser zero.
Catálogo de métodos
| Método | Assinatura | Descrição |
|---|---|---|
new |
WaitGroup::new() -> *WaitGroup |
Cria um wait group vazio com contador zero. |
add |
fn add(self: *WaitGroup, n: i32) |
Aumenta o contador em n (passe um número positivo). |
done |
fn done(self: *WaitGroup) |
Decrementa em um; quando chega a zero, as chamadas bloqueadas de wait() retornam. |
wait |
fn wait(self: *WaitGroup) |
Bloqueia até o contador chegar a zero. |
free |
fn free(self: *WaitGroup) |
Libera o wrapper. |
add / done / wait
O padrão canônico: incrementa o contador, faz spawn desse número de workers (cada um com defer wg.done()), depois bloqueia em wait().
import stdlib::sync::*;
fn worker(wg: *WaitGroup) {
defer wg.done();
// ...executa o trabalho...
}
fn main() -> i32 {
let wg: *WaitGroup = WaitGroup::new();
defer wg.free();
wg.add(3);
for let i: i32 = 0; i < 3; i++ {
spawn worker(wg);
}
wg.wait(); // retorna quando os 3 workers chamarem done()
println!("all workers finished");
return 0;
}
Casos extremos: grupo vazio e rebalanceamento
wait() em um grupo recém-criado (contador zero) é uma operação nula. Cada add(n) deve ser balanceado por n chamadas a done() antes de wait() desbloquear:
import stdlib::sync::*;
fn main() -> i32 {
let wg: *WaitGroup = WaitGroup::new();
defer wg.free();
wg.wait(); // contador já é 0 -> retorna imediatamente
println!("nothing to wait for");
wg.add(2);
wg.done();
wg.done();
wg.wait(); // balanceado -> retorna
println!("done");
return 0;
}
Tempo de vida e posse
Cada primitiva é uma alocação no heap que pertence a você. O construtor aloca, e somente free() libera — não há drop automático para esses ponteiros brutos. O idioma robusto é defer .free() logo após a construção:
let m: *Mutex<i32> = Mutex::new(0);
defer m.free();
let a: *Atomic = Atomic::new(0);
defer a.free();
let wg: *WaitGroup = WaitGroup::new();
defer wg.free();
Após free(), o ponteiro fica pendente — não o toque novamente.
Veja também
Canais e spawn/corrotinas (a unidade de concorrência que essas primitivas sincronizam, e a alternativa de passagem de mensagens ao estado compartilhado) são cobertos no livro da linguagem, não nesta referência da biblioteca padrão. Combine um WaitGroup com um canal para coletar resultados dos workers após wait().