MessagePipe 의 동시 해제는 남의 구독을 지운다
LoadAsync().ContinueWith(_ => subscription.Dispose()); // 스레드 풀에서 해제
void OnDestroy() => subscription.Dispose(); // 메인 스레드에서도 해제
한 스레드에서 같은 구독을 두 번 Dispose 하는 것은 안전합니다. 두 번째는 아무 일도 하지 않습니다. 그런데 두 스레드가 동시에 부르면 KeyNotFoundException 이 나거나, 예외도 없이 전혀 다른 구독이 끊깁니다.
MessagePipe 의 해제는 “이미 해제했나” 검사가 락 밖에 있고, 목록은 칸 번호만 보고 지웁니다. 둘이 겹치면 같은 칸 번호로 두 번 지우게 되고, 그 칸에 그사이 누가 들어왔느냐가 결과를 가릅니다.
이미 해제했는지 검사하는 줄이 락 밖에 있다
해제용 객체 Subscription 은 자기 칸 번호와 해제 표시 하나를 듭니다.
// Runtime/MessageBroker.cs (Subscription, 줄임)
public void Dispose()
{
if (!isDisposed) // ① 락 밖에서 검사
{
isDisposed = true; // ② 락 밖에서 표시
lock (core.gate)
{
if (!core.isDisposed)
core.handlers.Remove(subscriptionKey, true);
}
}
}
이 표시가 막는 것은 순서대로 두 번입니다. 두 스레드가 거의 동시에 오면 둘 다 ①에서 false 를 읽고 안으로 들어갑니다. 검사와 표시 사이의 틈입니다.
두 번째 Remove 는 칸에 누가 있는지 보지 않는다
목록의 Remove 는 락 안에서 칸이 비었는지만 확인합니다. 비었으면 예외를 던지고, 차 있으면 누구든 지웁니다.
같은 경합을 50,000번씩 돌린 결과입니다. 유니티는 플레이어 빌드입니다.
같은 구독을 동시에 Dispose, KeyNotFoundException 횟수 |
.NET 10 | Mono | IL2CPP |
|---|---|---|---|
| 스레드 2개 | 26번 | 45번 | 59번 |
| 스레드 4개 | 10번 | 44번 | 65번 |
| 스레드 8개 | 15번 | 69번 | 81번 |
유니티가 .NET 의 2~5배 자주 터졌습니다. 틈이 if (!isDisposed) 한 줄이라 스레드를 늘려도 빈도는 거의 늘지 않습니다.
남의 구독이 사라지는 쪽은 .NET 에서 50,000번 중 19번 나왔고, 유니티에서는 재현되지 않았습니다. 세 번째 스레드가 딱 그 틈에 칸을 받아야 해서 조건이 더 좁을 뿐, 틈은 같은 코드에 있습니다.
칸 번호를 고정해서 해제를 싸게 만든 구조가, 번호가 다른 구독에게 넘어간 뒤에는 그 구독을 지우게 합니다.
주의할 점
1. 같은 구독의 해제는 한 스레드로 모은다
안 지키면 생기는 문제. 비동기 작업의 콜백이 스레드 풀에서 해제하고 OnDestroy 도 메인 스레드에서 해제하면 두 해제가 겹칠 수 있습니다. 운이 나쁘면 KeyNotFoundException, 더 나쁘면 그사이 들어온 다른 구독이 지워집니다. 지워진 쪽은 그 뒤로 메시지를 받지 못하고 로그도 남지 않습니다.
이렇게 쓴다. 해제는 메인 스레드 한 곳에서 합니다. 비동기 콜백에서 해제해야 하면 메인 스레드로 돌아온 뒤에 합니다.
2. 해제용 객체를 모은 묶음도 한 스레드에서 Dispose 한다
안 지키면 생기는 문제. DisposableBag 이 만든 묶음을 두 스레드가 Dispose 하면 안의 구독 하나하나가 같은 경합을 겪습니다. 묶었다고 경합이 사라지지 않습니다.
이렇게 쓴다. 묶음의 Dispose 도 소유한 오브젝트의 메인 스레드 정리 코드(OnDestroy 등) 한 곳에서만 부릅니다.
정리
MessagePipe 구독을 두 스레드가 동시에 해제하면 같은 칸을 두 번 지웁니다.
- 해제 표시의 검사와 쓰기가 락 밖이라 둘 다 안으로 들어갑니다.
- 칸이 비어 있으면
KeyNotFoundException, 다른 구독이 들어와 있으면 그 구독이 조용히 사라집니다. - 예외는 50,000번 중 .NET 10
26번, 유니티 4481번 났습니다.