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()를 잘못된 횟수만큼 호출함.
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
| 프리미티브 | 프로세스 간 동기화? | 속도 | 사용 위치 |
|---|---|---|---|
|
아니오 | 매우 빠름 | 한 프로세스 내의 스레드들 간 |
|
예 | 느림(비용 높음) | 다른 프로세스의 스레드들 간 |
|
예 (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 예외를 받을 수 있는데, 이는 리소스 상태가 불일치할 수 있다는 신호야.
GO TO FULL VERSION