1. Вступ
Згадаймо приклад із попередньої лекції: два потоки в нашому, ще доволі простому, застосунку інкрементують спільний лічильник, але підсумкове значення не завжди збігається з очікуваним. Для захисту цього лічильника ми вже використовували ключове слово 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("Критичну секцію звільнено.");
}
}
}
}
Спробуйте:
- Відкрийте два вікна з цим застосунком.
- Запустіть обидва — другий чекатиме, доки не натиснете 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
| Примітив | Міжпроцесна синхронізація? | Швидкість | Де використовувати |
|---|---|---|---|
|
Ні | Дуже висока | Між потоками в одному процесі |
|
Так | Нижча (дорожчий) | Між потоками різних процесів |
|
Так (у 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 (це ознака помилки й потенційно неконсистентного стану ресурсу).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ