Coding Memo
싱글턴 멀티 스레드 주의 본문
"멀티 스레드 환경에서 싱글턴 클래스의 생성자도 동시에 여러 스레드가 접근 할 수 있다는 것을 기억하자."
싱글턴 패턴 사용시에 스레드에 주의해야 한다.
당연하고 또 당연한 말이고 쉽게 찾을 수 있는 에러이기도 하지만 여기에 간단하게 작성해본다.
Singleton이란 싱글턴 클래스가 있다고 가정하자.
먼저 내가 겪었던 상황은 다음과 같다.
- 싱글턴 클래스가 Only-Read로만 사용되기 때문에 멀티스레드 환경에서의 다른 lock 처리를 해주지 않았다.
- 어떤 스레드 하나가 데이터를 받으면 Singleton .Instance.method()를 호출한다.
- 바로 위 상황이 아니면 다른 코드에서 이 싱글턴 클래스를 사용하지 않았다.
- 이 싱글턴 클래스는 생성자에서 처음이자 마지막으로 콜랙션에 데이터를 추가한다. (이후는 only-read)
위 상황에서 문제가 일어날 수 있는 부분은 다음과 같다.
- 여러 스레드가 동시에 Singleton.Instance.method()를 호출할 수 있다.
- Instance 호출(참조)가 처음이기 때문에 Singleton에 대한 생성자가 호출된다.
- 따라서 생성자가 중복되어 호출 될 수 있다!
(내가 이 부분을 간과했던 이유아닌 이유는 `어차피 읽기 전용 클래스인데 굳이 lock을 걸필요도 없고, 이에 신경 쓸 필요도 없겠지. ` 였다...)
아래 클래스를 보자.
생성자에서 최초로 _messageTypes와 _handlers에 데이터를 넣는다.
public class MessageManager
{
...
#region Singleton
static MessageManager _instance = null;
public static MessageManager Instance
{
get
{
if (_instance == null) _instance = new MessageManager();
return _instance;
}
}
#endregion
readonly Dictionary<ushort, MessageParser> _messageTypes = new Dictionary<ushort, MessageParser>();
readonly Dictionary<ushort, Action<IMessage, Session>> _handlers = new Dictionary<ushort, Action<IMessage, Session>>();
MessageManager()
{
... // TODO with _messageTypes and _handlers.
}
public void HandlePacket()
{
...
}
...
}
하나의 스레드가 안정적으로 MessageManager.Instance.HandlePacket()을 호출했을 때 (즉, _instance = new MessageManager()까지 혼자 안정적으로 수행했을 때)는 당연히 생성자가 한번 호출이 될 것이다.
그러나 만약 여러 스레드가 최초로(_instance가 아직 null인 상태) MessageManager.Instance.HandlePacket()을 호출하게 된다면, 한 스레드가 생성자를 실행하고 있을 때 다른 스레드는 _instance가 아직 null값이므로 다시 생성자를 호출하게 될 것이다.
이에 같은 코드가 실행되면서 (critical section이든 아니든 간에) _messageTypes나 _handlers에 중복된 key를 가진 원소를 넣으려고 하면서 다음 예외가 발생한다.
System.ArgumentException: An item with the same key has already been added.
해결
명시적으로 생성자의 내용을 Init()이라는 함수로 따로 작성하여 서버 실행 전에 MessageManager.Instance.Init()을 실행했다.
멀티스레드 환경이 되기 전에 어떤 방법으로든 미리 _instance에 값을 할당하는 것이 좋은 방법인 것 같다.
(싱글턴이라고 클래스 인스턴스 접근 자체에 lock을 쓰는 것은 하지 않는 것이 좋겠다.)
'Language > C#' 카테고리의 다른 글
BitConverter, 직렬화 (little-endian, big-endian) (0) | 2023.12.06 |
---|---|
Environment.TickCount 오버플로우 (대안) (0) | 2023.11.29 |
에코 서버 (TcpClient, async/await 이용) (0) | 2023.11.09 |
[C#] .NET 환경에서 지원하는 C# 버전 (0) | 2023.08.14 |
.NET Framework, .NET Core, .NET Standard (0) | 2023.08.14 |