Coding Memo

싱글턴 멀티 스레드 주의 본문

Language/C#

싱글턴 멀티 스레드 주의

minttea25 2023. 11. 23. 23:07

"멀티 스레드 환경에서 싱글턴 클래스의 생성자도 동시에 여러 스레드가 접근 할 수 있다는 것을 기억하자."

 

싱글턴 패턴 사용시에 스레드에 주의해야 한다. 

당연하고 또 당연한 말이고 쉽게 찾을 수 있는 에러이기도 하지만 여기에 간단하게 작성해본다.


 

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을 쓰는 것은 하지 않는 것이 좋겠다.)