CodeGym /행동 /C# SELF /동기화를 위한 뮤텍스: Mutex

동기화를 위한 뮤텍스: Mutex

C# SELF
레벨 56 , 레슨 2
사용 가능

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("임계 영역이 해제됨.");
            }
        }
    }
}

해보기:

  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()를 잘못된 횟수만큼 호출함.
WaitOne()보다 더 많이 ReleaseMutex()를 호출하면 ApplicationException이 발생해.

실수 #3: 자신이 잡지 않은 Mutex를 해제하려 함.
뮤텍스는 그것을 잡은 스레드에 "바인드"돼 있어. 오직 그 스레드만 ReleaseMutex()를 호출할 권한이 있어. 다른 스레드가 해제하려고 하면 .NET이 예외를 던질 거야.

실수 #4: 단순히 lock으로 충분한 곳에서 Mutex를 씀.
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 예외를 받을 수 있는데, 이는 리소스 상태가 불일치할 수 있다는 신호야.
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION