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
- Zig instalado (versão 0.13+). Veja o guia de instalação
- Conhecimento de threads em Zig
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
Sempre use
defer self.mutex.unlock(): Garante que o mutex será liberado mesmo em caso de erro ou return antecipado.Minimize seções críticas: Mantenha o código dentro do lock o mais curto possível.
Evite deadlocks: Sempre adquira múltiplos mutexes na mesma ordem.
Considere alternativas: Para contadores simples, operações atômicas podem ser mais eficientes.
Não anide locks desnecessariamente: Evite chamar funções que adquirem locks dentro de seções já protegidas.
Receitas Relacionadas
- Como criar threads em Zig - Fundamentos de threads
- Como usar operações atômicas em Zig - Alternativa sem lock
- Como usar thread pool em Zig - Pool de threads
- Como usar canais de comunicação em Zig - Comunicação segura