1. Introdução
Vamos relembrar o exemplo da palestra anterior: duas threads no nosso aplicativo ainda simples incrementam um contador compartilhado, mas o valor final nem sempre bate com o esperado. Para proteger esse contador a gente já usou a palavra-chave lock (ou, se for mais preciso, Monitor), que serve para sincronização apenas dentro de um processo. Mas o que fazer se o seu programa não for o único que quer usar o recurso? Por exemplo, você escreve um super-serviço rodando em duas instâncias, e ambas querem escrever no mesmo arquivo… ou controlar um hardware, tipo a porta da impressora? A solução é o bom e velho mutex (de mutual exclusion — "exclusão mútua").
Conceito
Mutex é um primitivo de sincronização que não só limita o acesso a um recurso entre threads dentro de um processo, mas também permite coordenar acesso entre processos diferentes na mesma máquina. Imagine uma plaquinha grande de "Ocupado" na porta da sala de reuniões, que tanto funcionários quanto visitantes conseguem ver.
No .NET isso é feito pela classe System.Threading.Mutex.
Quando o Mutex realmente é necessário:
- Quando você precisa sincronizar acesso entre threads em processos diferentes (por exemplo, dois aplicativos separados trabalhando no mesmo arquivo).
- Quando o recurso é tão valioso e indivisível que nem o contexto do processo pode compartilhar direitos de acesso.
Para sincronizar apenas entre threads de um mesmo processo normalmente usa-se lock (Monitor). O Mutex é preferível para sincronização entre processos, porque é mais pesado e mais lento.
Como funciona o Mutex
flowchart TD
A(Processo 1) --|Solicita|--> M(Mutex)
B(Processo 2) --|Solicita|--> M
M --|Permite só um|--> R(Recurso compartilhado)
A --|Libera|--> M
B --|Depois de liberar|--> M
2. Sintaxe básica de trabalho com Mutex
Criação
Mutex é criado tão simplesmente quanto a maioria das outras classes de sincronização:
using System.Threading;
Mutex mutex = new Mutex();
Métodos principais
- WaitOne() — tentativa de capturar o mutex; se não conseguir — a thread fica bloqueada até que outro libere o mutex.
- ReleaseMutex() — libera o mutex, permitindo que outras threads ou processos entrem na seção crítica.
Exemplo simples: sincronização entre threads dentro de um processo
using System;
using System.Threading;
class Program
{
static Mutex mutex = new Mutex();
static void Main()
{
Thread t1 = new Thread(PrintNumbers);
Thread t2 = new Thread(PrintNumbers);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
static void PrintNumbers()
{
for (int i = 0; i < 5; i++)
{
mutex.WaitOne(); // Entrar na seção crítica
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: {i}");
mutex.ReleaseMutex(); // Sair da seção crítica
Thread.Sleep(100); // Para visualização
}
}
}
Nesse exemplo ambas as threads acessam o console alternadamente para imprimir.
3. Sincronização entre processos com mutex nomeado
Para a "artilharia pesada" — sincronização entre processos — usa-se um mutex nomeado. Você dá um nome, e todos os processos na máquina podem referenciar o mesmo mutex.
Mutex mutex = new Mutex(false, "MyApp_Mutex");
Parâmetros do construtor:
- Primeiro parâmetro (bool initiallyOwned) — se a thread quer capturar o mutex imediatamente após a criação. Normalmente — false.
- Segundo parâmetro — o nome do mutex. Todos os processos que usam um mutex com esse nome referenciam o mesmo objeto, por exemplo "MyApp_Mutex".
Exemplo: dois aplicativos com o mesmo Mutex
Execute o mesmo programa em duas janelas diferentes para ver o efeito.
using System;
using System.Threading;
class Program
{
static void Main()
{
using (Mutex mutex = new Mutex(false, "MySuperUniqueMutexName"))
{
Console.WriteLine("Tentando entrar na seção crítica...");
mutex.WaitOne(); // Espera até que outro processo libere o mutex
try
{
Console.WriteLine("Seção crítica ocupada por este processo.");
Console.WriteLine("Pressione Enter para sair da seção crítica.");
Console.ReadLine();
}
finally
{
mutex.ReleaseMutex();
Console.WriteLine("Seção crítica liberada.");
}
}
}
}
Experimente:
- Abra duas janelas com esse aplicativo.
- Execute ambos — o segundo vai esperar até você pressionar Enter no primeiro.
4. Execução simultânea do aplicativo
Mutex é frequentemente usado para limitar quantas instâncias do aplicativo podem rodar ao mesmo tempo. Exemplo: "Ei, usuário, da próxima vez não abra duas cópias da calculadora!"
using System;
using System.Threading;
class Program
{
static void Main()
{
bool createdNew;
using (Mutex mutex = new Mutex(true, "CalculatorAppInstanceMutex", out createdNew))
{
if (!createdNew)
{
Console.WriteLine("Aplicativo já está em execução!");
return;
}
Console.WriteLine("Aplicativo iniciado com sucesso. Para sair, pressione Enter.");
Console.ReadLine();
}
}
}
Esse padrão é comum em aplicativos desktop: a primeira instância inicia, a segunda apenas notifica e encerra. O exemplo oficial da Microsoft está na documentação.
5. Erros típicos ao trabalhar com Mutex
Erro nº1: esqueceu de chamar ReleaseMutex().
Se a thread capturou o mutex (WaitOne()), mas não chamou ReleaseMutex() (caiu em uma exceção ou simplesmente esqueceu), nenhum outro thread ou processo poderá entrar até que a thread "esquecida" termine. Isso pode causar deadlock. Bom estilo é sempre usar try-finally:
mutex.WaitOne();
try
{
// seção crítica
}
finally
{
mutex.ReleaseMutex();
}
Erro nº2: número incorreto de chamadas a ReleaseMutex().
Se você chamar ReleaseMutex() mais vezes do que chamou WaitOne(), será lançada uma exceção ApplicationException.
Erro nº3: tentar liberar um Mutex que não é seu.
O mutex é "amarrado" à thread que o capturou. Só essa thread tem o direito de chamar ReleaseMutex(). Se outra thread tentar liberar, o .NET vai lançar uma exceção.
Erro nº4: usar Mutex quando lock seria suficiente.
Mutex é mais lento que o lock, porque pode atuar entre processos e envolve chamadas ao sistema. Recomendação: se não precisa de sincronização entre processos — use lock.
Erro nº5: nome ruim para o mutex.
Se escolher um nome muito simples (por exemplo, "MyMutex"), você pode acidentalmente "se conectar" com um programa alheio que usa o mesmo nome. Melhor usar nomes únicos (por exemplo, incluindo o nome da sua empresa ou do aplicativo).
6. Dicas úteis
WaitOne(timeout)
Dá para especificar um timeout ao esperar pelo mutex:
if (mutex.WaitOne(5000)) // espera até 5 segundos
{
try { /* ... */ }
finally { mutex.ReleaseMutex(); }
}
else
{
Console.WriteLine("Não foi possível obter acesso ao recurso em 5 segundos!");
}
Mutex.TryOpenExisting
Se precisar anexar a um mutex já existente, use o método estático:
if (Mutex.TryOpenExisting("DiaryFileWriteMutex", out Mutex existingMutex))
{
// agora existingMutex é uma referência ao mutex existente
}
Separação entre usuários
Por padrão o mutex nomeado está disponível para todos os usuários com permissões para criar objetos do sistema. Se precisar de controle mais rígido — use o construtor com MutexSecurity.
Tabela comparativa: lock/Monitor, Mutex, Semaphore
| Primitivo | Sincronização entre processos? | Velocidade | Onde usar |
|---|---|---|---|
|
Não | Muito alta | Entre threads em um processo |
|
Sim | Mais baixa (mais cara) | Entre threads de processos diferentes |
|
Sim (no caso de NamedSemaphore) | Comparável ao Mutex | Quando precisa limitar número de threads |
Mais sobre semáforos na próxima palestra.
Estados do Mutex
stateDiagram-v2
[*] --> Unowned
Unowned --> Owned : WaitOne()
Owned --> Owned : WaitOne() (reentrante)
Owned --> Unowned : ReleaseMutex()
Owned --> Abandoned : thread "morreu", não chamou ReleaseMutex()
Abandoned --> Unowned
- Unowned — ninguém possui o mutex.
- Owned — o mutex foi capturado por uma thread.
- Abandoned — a thread terminou inesperadamente sem chamar ReleaseMutex(). O próximo que receber o mutex vai receber uma exceção AbandonedMutexException (isso indica erro e possível estado inconsistente do recurso).
GO TO FULL VERSION