1. Introducción
Recordemos el ejemplo de la lección anterior: dos threads en nuestra aplicación todavía simple incrementan un contador compartido, pero el valor final no siempre coincide con el esperado. Para proteger ese contador ya usamos la palabra clave lock (o, para ser precisos, Monitor), que sirve para sincronizar solo dentro de un mismo proceso. Pero ¿qué hacer si tu programa no es el único que quiere usar el recurso? Por ejemplo, estás escribiendo algún superservicio ejecutado en dos instancias y ambos quieren escribir en el mismo archivo… o controlar hardware, como el puerto de una impresora. Aquí entra en juego el buen y viejo mutex (de mutual exclusion — «exclusión mutua»).
Concepto
Un mutex es un primitivo de sincronización que no solo limita el acceso a un recurso entre threads dentro de un mismo proceso, sino que también permite coordinar el acceso entre procesos distintos en la misma máquina. Imagínatelo como un gran cartel de «Ocupado» en la puerta de una sala de reuniones, que pueden ver tanto empleados como visitantes.
En .NET para esto está la clase System.Threading.Mutex.
Cuándo realmente necesitas Mutex:
- Cuando sincronizas acceso entre threads en procesos diferentes (por ejemplo, dos aplicaciones independientes trabajan sobre el mismo archivo).
- Cuando el recurso es tan valioso e indivisible que ni siquiera el contexto de proceso debería compartir derechos de acceso.
Para sincronizar solo entre threads de un mismo proceso normalmente se usa lock (Monitor). Mutex es preferible para sincronización entre procesos porque es más pesado y más lento.
Cómo funciona Mutex
flowchart TD
A(Proceso 1) --|Solicita|--> M(Mutex)
B(Proceso 2) --|Solicita|--> M
M --|Permite solo a uno|--> R(Recurso compartido)
A --|Libera|--> M
B --|Tras liberación|--> M
2. Sintaxis básica para trabajar con Mutex
Creación
Mutex se crea tan fácil como la mayoría de las demás clases de sincronización:
using System.Threading;
Mutex mutex = new Mutex();
Métodos principales
- WaitOne() — intento de tomar el mutex; si no se puede — el thread se bloquea hasta que otro lo libere.
- ReleaseMutex() — libera el mutex, permitiendo que otros threads o procesos entren en la sección crítica.
Ejemplo sencillo: sincronización entre threads dentro de un proceso
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 en la sección crítica
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: {i}");
mutex.ReleaseMutex(); // Salir de la sección crítica
Thread.Sleep(100); // Para mayor claridad
}
}
}
En este ejemplo ambos threads acceden a la consola por turnos para imprimir.
3. Sincronización entre procesos con un mutex nombrado
Para la verdadera «artillería pesada» — sincronización entre procesos — se usa un mutex nombrado. Le das un nombre y todos los procesos en el equipo pueden referirse a él.
Mutex mutex = new Mutex(false, "MyApp_Mutex");
Parámetros del constructor:
- El primer parámetro (bool initiallyOwned) — si el thread quiere tomar el mutex inmediatamente tras la creación. Normalmente — false.
- El segundo parámetro — el nombre del mutex. Todos los procesos que usan un mutex con ese nombre se refieren al mismo objeto, por ejemplo "MyApp_Mutex".
Ejemplo: dos aplicaciones con un mismo Mutex
Arranca el mismo programa en dos ventanas distintas para ver el efecto.
using System;
using System.Threading;
class Program
{
static void Main()
{
using (Mutex mutex = new Mutex(false, "MySuperUniqueMutexName"))
{
Console.WriteLine("Intentando entrar en la sección crítica...");
mutex.WaitOne(); // Esperamos hasta que otro proceso libere el mutex
try
{
Console.WriteLine("Sección crítica ocupada por este proceso.");
Console.WriteLine("Pulsa Enter para salir de la sección crítica.");
Console.ReadLine();
}
finally
{
mutex.ReleaseMutex();
Console.WriteLine("Sección crítica liberada.");
}
}
}
}
Pruébalo:
- Abre dos ventanas con esta aplicación.
- Ejecuta ambas — la segunda esperará hasta que pulses Enter en la primera.
4. Ejecutar la aplicación simultáneamente
Mutex se usa a menudo para limitar el número de instancias ejecutándose al mismo tiempo. Por ejemplo: «Oye, usuario, la próxima vez no abras dos copias de la 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("¡La aplicación ya está en ejecución!");
return;
}
Console.WriteLine("La aplicación arrancó correctamente. Pulsa Enter para salir.");
Console.ReadLine();
}
}
}
Este patrón es común en aplicaciones de escritorio: la primera instancia se inicia, la segunda solo informa y termina. Un ejemplo oficial de Microsoft está en la documentación.
5. Errores típicos al trabajar con Mutex
Error Nº1: olvidarse de llamar a ReleaseMutex().
Si un thread tomó correctamente el mutex (WaitOne()) pero no llamó a ReleaseMutex() (cayó por una excepción o simplemente se olvidó), ningún otro thread o proceso podrá entrar hasta que el thread «olvidadizo» termine. Esto puede causar deadlock (bloqueo mutuo). Buen estilo: usar siempre try-finally:
mutex.WaitOne();
try
{
// sección crítica
}
finally
{
mutex.ReleaseMutex();
}
Error Nº2: número incorrecto de llamadas a ReleaseMutex().
Si llamas a ReleaseMutex() más veces de las que llamaste a WaitOne(), se lanzará una excepción ApplicationException.
Error Nº3: intentar liberar un Mutex que no es tuyo.
El mutex está «ligado» al thread que lo tomó. Solo ese thread tiene derecho a llamar a ReleaseMutex(). Si otro thread intenta liberarlo, .NET lanzará una excepción.
Error Nº4: usar Mutex donde basta con lock.
Mutex es más lento que un lock normal, porque puede funcionar entre procesos y requiere llamadas al sistema. Recomendación: si no necesitas sincronización entre procesos — usa lock.
Error Nº5: elegir un mal nombre para el mutex.
Si eliges un nombre demasiado simple (por ejemplo, "MyMutex"), podrías coincidir con un programa ajeno que use el mismo nombre. Mejor usa nombres únicos (por ejemplo, con el nombre de tu empresa o aplicación).
6. Matices útiles
WaitOne(timeout)
Puedes especificar un timeout al esperar el mutex:
if (mutex.WaitOne(5000)) // esperamos hasta 5 segundos
{
try { /* ... */ }
finally { mutex.ReleaseMutex(); }
}
else
{
Console.WriteLine("¡No se pudo acceder al recurso en 5 segundos!");
}
Mutex.TryOpenExisting
Si necesitas conectarte a un mutex que ya existe, usa el método estático:
if (Mutex.TryOpenExisting("DiaryFileWriteMutex", out Mutex existingMutex))
{
// ahora existingMutex es una referencia al mutex existente
}
Separación entre usuarios
Por defecto, un mutex nombrado está disponible para todos los usuarios con permisos para crear objetos del sistema. Si necesitas un control más estricto — usa el constructor con MutexSecurity.
Tabla comparativa: lock/Monitor, Mutex, Semaphore
| Primitivo | ¿Sincronización entre procesos? | Velocidad | Dónde usar |
|---|---|---|---|
|
No | Muy alta | Entre threads en un proceso |
|
Sí | Más baja (más costosa) | Entre threads de procesos diferentes |
|
Sí (en el caso de NamedSemaphore) | Comparable con Mutex | Cuando necesitas limitar el número de threads |
Más sobre semáforos — en la siguiente lección.
Estados de Mutex
stateDiagram-v2
[*] --> Unowned
Unowned --> Owned : WaitOne()
Owned --> Owned : WaitOne() (reentrante)
Owned --> Unowned : ReleaseMutex()
Owned --> Abandoned : el thread "murió" sin llamar ReleaseMutex()
Abandoned --> Unowned
- Unowned — nadie posee el mutex.
- Owned — el mutex está tomado por un thread.
- Abandoned — el thread terminó de forma inesperada sin llamar a ReleaseMutex(). El siguiente que obtenga el mutex recibirá una excepción AbandonedMutexException (esto indica un error y un posible estado inconsistente del recurso).
GO TO FULL VERSION