JavaRush /Курсы /C# SELF /Мьютексы для синхронизации:

Мьютексы для синхронизации: Mutex

C# SELF
56 уровень , 2 лекция
Открыта

1. Введение

Вспомним пример из предыдущей лекции: два потока в нашем пока ещё простом приложении инкрементируют общий счётчик, но итоговое значение не всегда совпадает с ожидаемым. Для защиты этого счётчика мы уже использовали keyword lock (или, если быть точнее, Monitor), который подходит для синхронизации только внутри одного процесса. Но что делать, если ваша программа — не единственная, кто хочет попользоваться ресурсом? Например, вы пишете какой-нибудь суперсервис, запущенный в двух экземплярах, и оба хотят писать в один файл… или распоряжаться оборудованием, например, портом принтера? На помощь приходит старый добрый мьютекс (от mutual exclusion — «взаимное исключение»).

Концепция

Мьютекс — это примитив синхронизации, который не только ограничивает доступ к ресурсу между потоками внутри одного процесса, но и позволяет координировать доступ между разными процессами на одной и той же машине. Представьте его как большую табличку «Занято» перед переговорной комнатой, которую видят и сотрудники, и посетители.

В .NET для этого предназначен класс System.Threading.Mutex.

Когда Mutex действительно нужен:

  • Когда вы синхронизируете доступ между потоками в разных процессах (например, два отдельных приложения работают над одним и тем же файлом).
  • Когда ресурс настолько ценный и неделимый, что даже контекст процесса не может разделять права на доступ.

Для синхронизации только между потоками одного процесса обычно используется lock (Monitor). Mutex предпочтительно использовать для межпроцессной синхронизации, потому что он тяжелее и медленнее.

Как работает Mutex

flowchart TD
    A(Процесс 1) --|Запрашивает|--> M(Mutex)
    B(Процесс 2) --|Запрашивает|--> M
    M --|Разрешение только одному|--> R(Общий ресурс)
    A --|Освобождает|--> M
    B --|После освобождения|--> M

2. Основной синтаксис работы с Mutex

Создание

Mutex создаётся так же просто, как и большинство других классов синхронизации:

using System.Threading;

Mutex mutex = new Mutex();

Основные методы

  • WaitOne() — попытка захватить мьютекс; если не получилось — поток блокируется до освобождения мьютекса кем-то другим.
  • ReleaseMutex() — освобождает мьютекс, позволяя другим потокам или процессам войти в критическую секцию.

Простейший пример: синхронизация между потоками внутри одного процесса

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(); // Войти в критическую секцию
            Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: {i}");
            mutex.ReleaseMutex(); // Выйти из критической секции
            Thread.Sleep(100); // Для наглядности
        }
    }
}

В этом примере оба потока по очереди получают доступ к консоли для печати.

3. Межпроцессная синхронизация с именованным мьютексом

Для настоящей «тяжёлой артиллерии» — синхронизации между процессами — используется именованный мьютекс. Вы даёте ему имя, и все процессы на компьютере могут обратиться к нему.

Mutex mutex = new Mutex(false, "MyApp_Mutex");

Параметры конструктора:

  • Первый параметр (bool initiallyOwned) — хочет ли поток сразу захватить мьютекс после создания. Обычно — false.
  • Второй параметр — имя мьютекса. Все процессы, которые используют мьютекс с таким именем, обращаются к одному и тому же объекту, например "MyApp_Mutex".

Пример: два приложения с одним Mutex

Запустите одну и ту же программу из двух разных окон, чтобы увидеть эффект.

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        using (Mutex mutex = new Mutex(false, "MySuperUniqueMutexName"))
        {
            Console.WriteLine("Попытка войти в критическую секцию...");
            mutex.WaitOne(); // Ждём, пока другой процесс освободит мьютекс
            try
            {
                Console.WriteLine("Критическая секция занята этим процессом.");
                Console.WriteLine("Нажмите Enter, чтобы выйти из критической секции.");
                Console.ReadLine();
            }
            finally
            {
                mutex.ReleaseMutex();
                Console.WriteLine("Критическая секция освобождена.");
            }
        }
    }
}

Попробуйте:

  1. Откройте два окна с этим приложением.
  2. Запустите оба — второй будет ждать, пока вы не нажмёте Enter в первом.

4. Одновременный запуск приложения

Mutex часто применяют для ограничения количества одновременно запущенных экземпляров. Например: «Эй, пользователь, в следующий раз не открывай две копии калькулятора!»

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("Приложение уже запущено!");
                return;
            }

            Console.WriteLine("Приложение успешно стартовало. Для выхода нажмите Enter.");
            Console.ReadLine();
        }
    }
}

Этот паттерн часто можно встретить в десктопных приложениях: первый экземпляр запускается, второй — просто сообщает об этом и завершает работу. Официальный пример Microsoft — в документации.

5. Типичные ошибки при работе с Mutex

Ошибка №1: забыли вызвать ReleaseMutex().
Если поток успешно захватил мьютекс (WaitOne()), но не вызвал ReleaseMutex() (упал с исключением или просто забыли), то ни один другой поток или процесс не сможет пройти внутрь, пока «забывчивый» поток не завершится. Это может вызвать deadlock (взаимную блокировку). Хороший стиль — всегда использовать try-finally:

mutex.WaitOne();
try
{
    // критическая секция
}
finally
{
    mutex.ReleaseMutex();
}

Ошибка №2: неправильное количество вызовов ReleaseMutex().
Если вызвать ReleaseMutex() больше раз, чем вызывалось WaitOne(), будет выброшено исключение ApplicationException.

Ошибка №3: попытка освободить не свой Mutex.
Мьютекс «привязан» к потоку, который его захватил. Только этот поток имеет право вызвать ReleaseMutex(). Если другой поток попытается освободить его, .NET выбросит исключение.

Ошибка №4: использование Mutex там, где достаточно lock.
Mutex работает медленнее, чем обычный lock, поскольку может работать между процессами и требует системных вызовов. Рекомендация: если не нужна межпроцессная синхронизация — используйте lock.

Ошибка №5: неудачное имя мьютекса.
Если выбрать слишком простое имя (например, "MyMutex"), можно случайно «подружиться» с чужой программой, которая его тоже использует. Лучше использовать уникальные имена (например, с названием своей компании или приложения).

6. Полезные нюансы

WaitOne(timeout)

Можно задавать таймаут ожидания мьютекса:

if (mutex.WaitOne(5000)) // ждем до 5 секунд
{
    try { /* ... */ }
    finally { mutex.ReleaseMutex(); }
}
else
{
    Console.WriteLine("Не удалось получить доступ к ресурсу за 5 секунд!");
}

Mutex.TryOpenExisting

Если нужно подсоединиться к уже существующему мьютексу, используйте статический метод:

if (Mutex.TryOpenExisting("DiaryFileWriteMutex", out Mutex existingMutex))
{
    // теперь existingMutex — это ссылка на существующий мьютекс
}

Разделение между пользователями

По умолчанию именованный мьютекс доступен всем пользователям с правами на создание системных объектов. Если требуется более строгий контроль — используйте конструктор с MutexSecurity.

Таблица сравнения: lock/Monitor, Mutex, Semaphore

Примитив Межпроцессная синхронизация? Скорость Где использовать
lock/Monitor
Нет Очень высокая Между потоками в одном процессе
Mutex
Да Ниже (дороже) Между потоками разных процессов
Semaphore
Да (у NamedSemaphore) Сравнимо с Mutex Когда нужно ограничить число потоков

Подробнее о семафорах — в следующей лекции.

Состояния Mutex

stateDiagram-v2
    [*] --> Unowned
    Unowned --> Owned : WaitOne()
    Owned --> Owned : WaitOne() (reentrant)
    Owned --> Unowned : ReleaseMutex()
    Owned --> Abandoned : поток "умер", не вызвав ReleaseMutex()
    Abandoned --> Unowned
  • Unowned — никто не владеет мьютексом.
  • Owned — мьютекс захвачен потоком.
  • Abandoned — поток внезапно завершился, не вызвав ReleaseMutex(). Следующий, кто получит мьютекс, получит исключение AbandonedMutexException (это признак ошибки и потенциально неконсистентного состояния ресурса).
2
Задача
C# SELF, 56 уровень, 2 лекция
Недоступна
Простая защита ресурса с помощью Mutex
Простая защита ресурса с помощью Mutex
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ