Como Usar Mutex para Sincronização em Zig

Introdução

Um Mutex (Mutual Exclusion) é um mecanismo de sincronização que garante que apenas uma thread acesse um recurso compartilhado por vez. Sem sincronização adequada, múltiplas threads acessando os mesmos dados podem causar data races e comportamento imprevisível.

Nesta receita, você aprenderá a usar std.Thread.Mutex para proteger dados compartilhados.

Pré-requisitos

Mutex Básico

Proteja um contador compartilhado entre threads:

const std = @import("std");

const ContadorSeguro = struct {
    valor: i64 = 0,
    mutex: std.Thread.Mutex = .{},

    pub fn incrementar(self: *ContadorSeguro) void {
        self.mutex.lock();
        defer self.mutex.unlock();

        self.valor += 1;
    }

    pub fn decrementar(self: *ContadorSeguro) void {
        self.mutex.lock();
        defer self.mutex.unlock();

        self.valor -= 1;
    }

    pub fn get(self: *ContadorSeguro) i64 {
        self.mutex.lock();
        defer self.mutex.unlock();

        return self.valor;
    }
};

fn worker(contador: *ContadorSeguro, n: u32) void {
    for (0..n) |_| {
        contador.incrementar();
    }
}

pub fn main() !void {
    var contador = ContadorSeguro{};

    const num_threads = 8;
    const ops_por_thread = 10000;

    var threads: [num_threads]std.Thread = undefined;

    for (&threads) |*t| {
        t.* = try std.Thread.spawn(.{}, worker, .{ &contador, ops_por_thread });
    }

    for (&threads) |t| {
        t.join();
    }

    const total = contador.get();
    const esperado: i64 = num_threads * ops_por_thread;
    std.debug.print("Total: {d} (esperado: {d})\n", .{ total, esperado });
}

Saída esperada

Total: 80000 (esperado: 80000)

Lista Thread-Safe com Mutex

Proteja uma estrutura de dados complexa:

const std = @import("std");

fn ListaSegura(comptime T: type) type {
    return struct {
        items: std.ArrayList(T),
        mutex: std.Thread.Mutex = .{},

        const Self = @This();

        pub fn init(allocator: std.mem.Allocator) Self {
            return .{
                .items = std.ArrayList(T).init(allocator),
            };
        }

        pub fn deinit(self: *Self) void {
            self.items.deinit();
        }

        pub fn append(self: *Self, valor: T) !void {
            self.mutex.lock();
            defer self.mutex.unlock();

            try self.items.append(valor);
        }

        pub fn len(self: *Self) usize {
            self.mutex.lock();
            defer self.mutex.unlock();

            return self.items.items.len;
        }

        pub fn get(self: *Self, index: usize) ?T {
            self.mutex.lock();
            defer self.mutex.unlock();

            if (index >= self.items.items.len) return null;
            return self.items.items[index];
        }
    };
}

fn adicionarItens(lista: *ListaSegura(u32), inicio: u32, quantidade: u32) void {
    for (0..quantidade) |i| {
        lista.append(inicio + @as(u32, @intCast(i))) catch return;
    }
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var lista = ListaSegura(u32).init(allocator);
    defer lista.deinit();

    const t1 = try std.Thread.spawn(.{}, adicionarItens, .{ &lista, 0, 100 });
    const t2 = try std.Thread.spawn(.{}, adicionarItens, .{ &lista, 100, 100 });
    const t3 = try std.Thread.spawn(.{}, adicionarItens, .{ &lista, 200, 100 });

    t1.join();
    t2.join();
    t3.join();

    std.debug.print("Total de itens: {d}\n", .{lista.len()});
}

Proteger Seções Críticas

Use mutex para operações que envolvem múltiplas etapas:

const std = @import("std");

const ContaBancaria = struct {
    saldo: f64 = 0,
    mutex: std.Thread.Mutex = .{},
    nome: []const u8,

    pub fn depositar(self: *ContaBancaria, valor: f64) void {
        self.mutex.lock();
        defer self.mutex.unlock();

        // Seção crítica: múltiplas operações que devem ser atômicas
        const saldo_anterior = self.saldo;
        self.saldo += valor;
        std.debug.print("{s}: Depósito R${d:.2} | Saldo: R${d:.2} -> R${d:.2}\n", .{
            self.nome, valor, saldo_anterior, self.saldo,
        });
    }

    pub fn sacar(self: *ContaBancaria, valor: f64) bool {
        self.mutex.lock();
        defer self.mutex.unlock();

        if (self.saldo < valor) {
            std.debug.print("{s}: Saque R${d:.2} NEGADO (saldo: R${d:.2})\n", .{
                self.nome, valor, self.saldo,
            });
            return false;
        }

        self.saldo -= valor;
        std.debug.print("{s}: Saque R${d:.2} | Saldo: R${d:.2}\n", .{
            self.nome, valor, self.saldo,
        });
        return true;
    }

    pub fn getSaldo(self: *ContaBancaria) f64 {
        self.mutex.lock();
        defer self.mutex.unlock();
        return self.saldo;
    }
};

fn transferir(origem: *ContaBancaria, destino: *ContaBancaria, valor: f64) void {
    // Lock ambas as contas em ordem consistente para evitar deadlock
    // Usar endereços de memória para definir a ordem
    const primeiro = if (@intFromPtr(origem) < @intFromPtr(destino)) origem else destino;
    const segundo = if (@intFromPtr(origem) < @intFromPtr(destino)) destino else origem;

    primeiro.mutex.lock();
    segundo.mutex.lock();
    defer primeiro.mutex.unlock();
    defer segundo.mutex.unlock();

    if (origem.saldo >= valor) {
        origem.saldo -= valor;
        destino.saldo += valor;
        std.debug.print("Transferência R${d:.2}: {s} -> {s}\n", .{
            valor, origem.nome, destino.nome,
        });
    }
}

fn operacoes(conta: *ContaBancaria) void {
    for (0..5) |_| {
        conta.depositar(100.0);
        _ = conta.sacar(50.0);
    }
}

pub fn main() !void {
    var conta_a = ContaBancaria{ .nome = "Conta A" };
    var conta_b = ContaBancaria{ .nome = "Conta B" };

    conta_a.depositar(1000.0);
    conta_b.depositar(500.0);

    const t1 = try std.Thread.spawn(.{}, operacoes, .{&conta_a});
    const t2 = try std.Thread.spawn(.{}, operacoes, .{&conta_b});

    t1.join();
    t2.join();

    std.debug.print("\nSaldo final {s}: R${d:.2}\n", .{ conta_a.nome, conta_a.getSaldo() });
    std.debug.print("Saldo final {s}: R${d:.2}\n", .{ conta_b.nome, conta_b.getSaldo() });
}

tryLock: Tentativa Não-Bloqueante

Use tryLock quando não quer esperar pelo mutex:

const std = @import("std");

const RecursoCompartilhado = struct {
    mutex: std.Thread.Mutex = .{},
    dados: u64 = 0,

    pub fn tentarProcessar(self: *RecursoCompartilhado, id: u32) bool {
        if (self.mutex.tryLock()) {
            defer self.mutex.unlock();

            // Simular processamento
            self.dados += 1;
            std.debug.print("Thread {d}: processou (dados = {d})\n", .{ id, self.dados });
            std.time.sleep(std.time.ns_per_ms * 10);
            return true;
        } else {
            std.debug.print("Thread {d}: recurso ocupado, pulando\n", .{id});
            return false;
        }
    }
};

fn worker(recurso: *RecursoCompartilhado, id: u32) void {
    var sucessos: u32 = 0;
    for (0..10) |_| {
        if (recurso.tentarProcessar(id)) {
            sucessos += 1;
        }
        std.time.sleep(std.time.ns_per_ms * 5);
    }
    std.debug.print("Thread {d}: {d}/10 operações bem sucedidas\n", .{ id, sucessos });
}

pub fn main() !void {
    var recurso = RecursoCompartilhado{};

    const t1 = try std.Thread.spawn(.{}, worker, .{ &recurso, 1 });
    const t2 = try std.Thread.spawn(.{}, worker, .{ &recurso, 2 });
    const t3 = try std.Thread.spawn(.{}, worker, .{ &recurso, 3 });

    t1.join();
    t2.join();
    t3.join();
}

Dicas e Boas Práticas

  1. Sempre use defer self.mutex.unlock(): Garante que o mutex será liberado mesmo em caso de erro ou return antecipado.

  2. Minimize seções críticas: Mantenha o código dentro do lock o mais curto possível.

  3. Evite deadlocks: Sempre adquira múltiplos mutexes na mesma ordem.

  4. Considere alternativas: Para contadores simples, operações atômicas podem ser mais eficientes.

  5. Não anide locks desnecessariamente: Evite chamar funções que adquirem locks dentro de seções já protegidas.

Receitas Relacionadas

Tutoriais Relacionados

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.