CodeGym /Cursos /C# SELF /Mutexes para sincronização:

Mutexes para sincronização: Mutex

C# SELF
Nível 56 , Lição 2
Disponível

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:

  1. Abra duas janelas com esse aplicativo.
  2. 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
lock/Monitor
Não Muito alta Entre threads em um processo
Mutex
Sim Mais baixa (mais cara) Entre threads de processos diferentes
Semaphore
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).
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION