CodeGym /Kursy /C# SELF /Mutesy do synchronizacji: ...

Mutesy do synchronizacji: Mutex

C# SELF
Poziom 56, Lekcja 2
Dostępny

1. Wprowadzenie

Przypomnijmy przykład z poprzedniej lekcji: dwa wątki w naszej na razie prostej aplikacji inkrementują wspólny licznik, ale końcowa wartość nie zawsze zgadza się z oczekiwaną. Do ochrony tego licznika już używaliśmy keyword lock (albo, dokładniej, Monitor), który nadaje się do synchronizacji tylko wewnątrz jednego procesu. Ale co zrobić, jeśli Twoja aplikacja — nie jest jedyna, która chce skorzystać z zasobu? Na przykład piszesz jakiś superserwis uruchomiony w dwóch instancjach i obie chcą zapisywać do tego samego pliku… albo zarządzać sprzętem, np. portem drukarki? Na pomoc przychodzi stary dobry mutex (od mutual exclusion — „wzajemne wykluczenie”).

Koncepcja

Mutex to prymityw synchronizacji, który nie tylko ogranicza dostęp do zasobu między wątkami w jednym procesie, ale także pozwala koordynować dostęp między różnymi procesami na tej samej maszynie. Wyobraź go sobie jako wielką tabliczkę „Zajęte” przed salą konferencyjną, którą widzą zarówno pracownicy, jak i goście.

W .NET do tego służy klasa System.Threading.Mutex.

Kiedy Mutex jest naprawdę potrzebny:

  • Kiedy synchronizujesz dostęp między wątkami w różnych procesach (np. dwie osobne aplikacje pracują na tym samym pliku).
  • Kiedy zasób jest tak cenny i niepodzielny, że nawet kontekst procesu nie może dzielić praw dostępu.

Do synchronizacji tylko między wątkami jednego procesu zwykle używa się lock (Monitor). Mutex warto używać do synchronizacji międzyprocesowej, bo jest cięższy i wolniejszy.

Jak działa Mutex

flowchart TD
    A(Proces 1) --|Żąda|--> M(Mutex)
    B(Proces 2) --|Żąda|--> M
    M --|Zezwolenie tylko jednemu|--> R(Wspólny zasób)
    A --|Zwalnia|--> M
    B --|Po zwolnieniu|--> M

2. Podstawowa składnia pracy z Mutex

Utworzenie

Mutex tworzy się tak samo prosto, jak większość innych klas synchronizacyjnych:

using System.Threading;

Mutex mutex = new Mutex();

Podstawowe metody

  • WaitOne() — próba złapania mutexa; jeśli się nie uda — wątek zostaje zablokowany aż ktoś inny zwolni mutex.
  • ReleaseMutex() — zwalnia mutex, pozwalając innym wątkom lub procesom wejść do sekcji krytycznej.

Najprostszy przykład: synchronizacja między wątkami w jednym procesie

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(); // Wejść do sekcji krytycznej
            Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: {i}");
            mutex.ReleaseMutex(); // Wyjść z sekcji krytycznej
            Thread.Sleep(100); // Dla przejrzystości
        }
    }
}

W tym przykładzie oba wątki na przemian dostają dostęp do konsoli, żeby wypisać dane.

3. Synchronizacja międzyprocesowa z nazwanym mutexem

Do prawdziwej „ciężkiej artylerii” — synchronizacji między procesami — używa się nazwanego mutexa. Nadajesz mu nazwę i wszystkie procesy na komputerze mogą odwoływać się do tego samego obiektu.

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

Parametry konstruktora:

  • Pierwszy parametr (bool initiallyOwned) — czy wątek chce od razu przejąć mutex po tworzeniu. Zazwyczaj — false.
  • Drugi parametr — nazwa mutexa. Wszystkie procesy, które używają mutexa o takiej nazwie, odwołują się do tego samego obiektu, np. "MyApp_Mutex".

Przykład: dwie aplikacje z jednym Mutex

Uruchom tę samą aplikację w dwóch różnych oknach, żeby zobaczyć efekt.

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        using (Mutex mutex = new Mutex(false, "MySuperUniqueMutexName"))
        {
            Console.WriteLine("Próba wejścia do sekcji krytycznej...");
            mutex.WaitOne(); // Czekamy, aż inny proces zwolni mutex
            try
            {
                Console.WriteLine("Sekcja krytyczna zajęta przez ten proces.");
                Console.WriteLine("Naciśnij Enter, żeby opuścić sekcję krytyczną.");
                Console.ReadLine();
            }
            finally
            {
                mutex.ReleaseMutex();
                Console.WriteLine("Sekcja krytyczna zwolniona.");
            }
        }
    }
}

Spróbuj:

  1. Otwórz dwa okna z tą aplikacją.
  2. Uruchom obie — drugi będzie czekał, aż naciśniesz Enter w pierwszym.

4. Równoczesne uruchamianie aplikacji

Mutex często używa się do ograniczenia liczby jednocześnie uruchomionych instancji aplikacji. Na przykład: „Hej, użytkowniku, następnym razem nie otwieraj dwóch kopii kalkulatora!”

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("Aplikacja jest już uruchomiona!");
                return;
            }

            Console.WriteLine("Aplikacja wystartowała pomyślnie. Aby zamknąć naciśnij Enter.");
            Console.ReadLine();
        }
    }
}

Ten wzorzec często spotyka się w aplikacjach desktopowych: pierwsza instancja startuje, druga — tylko informuje o tym i kończy działanie. Oficjalny przykład Microsoft — w dokumentacji.

5. Typowe błędy przy pracy z Mutex

Błąd nr 1: zapomniano wywołać ReleaseMutex().
Jeśli wątek poprawnie przejął mutex (WaitOne()), ale nie wywołał ReleaseMutex() (zawiesił się z wyjątkiem lub po prostu zapomniano), żaden inny wątek ani proces nie będzie mógł wejść, dopóki „zapominalski” wątek się nie zakończy. To może spowodować deadlock. Dobry styl — zawsze używać try-finally:

mutex.WaitOne();
try
{
    // sekcja krytyczna
}
finally
{
    mutex.ReleaseMutex();
}

Błąd nr 2: nieprawidłowa liczba wywołań ReleaseMutex().
Jeśli wywołać ReleaseMutex() więcej razy, niż było WaitOne(), zostanie rzucony wyjątek ApplicationException.

Błąd nr 3: próba zwolnić mutex, który nie należy do nas.
Mutex jest „przywiązany” do wątku, który go przejął. Tylko ten wątek ma prawo wywołać ReleaseMutex(). Jeśli inny wątek spróbuje go zwolnić, .NET wyrzuci wyjątek.

Błąd nr 4: używanie Mutex tam, gdzie wystarczy lock.
Mutex działa wolniej niż zwykły lock, ponieważ może działać między procesami i wymaga wywołań systemowych. Zalecenie: jeśli nie potrzebujesz synchronizacji międzyprocesowej — użyj lock.

Błąd nr 5: niefortunna nazwa mutexa.
Jeśli wybierzesz zbyt prostą nazwę (np. "MyMutex"), możesz przypadkowo „zaprzyjaźnić się” z cudzą aplikacją, która też jej używa. Lepiej stosować unikalne nazwy (np. z nazwą firmy albo aplikacji).

6. Przydatne niuanse

WaitOne(timeout)

Można podać timeout oczekiwania na mutex:

if (mutex.WaitOne(5000)) // czekamy do 5 sekund
{
    try { /* ... */ }
    finally { mutex.ReleaseMutex(); }
}
else
{
    Console.WriteLine("Nie udało się uzyskać dostępu do zasobu w ciągu 5 sekund!");
}

Mutex.TryOpenExisting

Jeśli trzeba podłączyć się do już istniejącego mutexa, użyj metody statycznej:

if (Mutex.TryOpenExisting("DiaryFileWriteMutex", out Mutex existingMutex))
{
    // teraz existingMutex — to referencja do istniejącego mutexa
}

Rozdzielenie między użytkownikami

Domyślnie nazwany mutex jest dostępny dla wszystkich użytkowników mających prawa do tworzenia obiektów systemowych. Jeśli potrzebna jest ścisła kontrola — użyj konstruktora z MutexSecurity.

Porównanie: lock/Monitor, Mutex, Semaphore

Prymitw Synchronizacja międzyprocesowa? Szybkość Gdzie używać
lock/Monitor
Nie Bardzo wysoka Między wątkami w jednym procesie
Mutex
Tak Niższa (droższa) Między wątkami w różnych procesach
Semaphore
Tak (u NamedSemaphore) Porównywalna z Mutex Kiedy trzeba ograniczyć liczbę wątków

Więcej o semaforach — w następnej lekcji.

Stany Mutex

stateDiagram-v2
    [*] --> Unowned
    Unowned --> Owned : WaitOne()
    Owned --> Owned : WaitOne() (reentrant)
    Owned --> Unowned : ReleaseMutex()
    Owned --> Abandoned : wątek "umarł", nie wywołując ReleaseMutex()
    Abandoned --> Unowned
  • Unowned — nikt nie jest właścicielem mutexa.
  • Owned — mutex został przejęty przez wątek.
  • Abandoned — wątek nagle zakończył się, nie wywołując ReleaseMutex(). Następny, kto otrzyma mutex, dostanie wyjątek AbandonedMutexException (to sygnał błędu i potencjalnie niespójnego stanu zasobu).
2
Zadanie
C# SELF, poziom 56, lekcja 2
Niedostępne
Prosta ochrona zasobu za pomocą Mutex
Prosta ochrona zasobu za pomocą Mutex
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION