Capítulo 26 14 min de leitura

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&lt;T&gt;

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;
}
output
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;
}
output
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;
}
output
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;
}
output
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().