반응형

어쩌다보니, 아직도 C# 서버로 포트폴리오 작업을 하고 있는데요.

 

클라이언트 작업과 서버 작업을 동시에 하다 보니, 생각보다 오래 걸립니다.

계속해서 기능을 추가하다보니, 오래 걸리네요..

 

일단 이번에 서버를 만들면서 어떻게 Lock Free 하게 만들 수 있을까 보다 보니,

 

C# 에도 동일한 Concurrent 와 Interlocked가 있더라구요.

 

주로 사용하는 ConcurrentDictionary 와 Interlocked 에 대해 간단히 정리합니다.


ConcurrentDictionary

System.Collections.Concurrent 네임스페이스에 포함된 스레드-안전(thread-safe) 컬렉션입니다.

 

기본적으로 여러 스레드가 동시에 읽기 및 쓰기를 시도하더라도 데이터 무결성을 유지하도록 설계되어 있답니다.

 

주요 특징

  1. 스레드 안전성 (Thread-Safety):
    • 여러 스레드가 동시에 데이터를 추가, 수정, 삭제해도 동기화 문제가 발생하지 않습니다.
    • 내부적으로 가벼운 락이나 락 없는 알고리즘을 사용하여 동시 접근을 처리합니다.
  2. 키-값 쌍 관리:
    • 키와 값을 기반으로 데이터를 저장하며, 기존 Dictionary와 같은 방식으로 작동합니다.
    • 데이터 추가, 업데이트, 삭제, 조회 등의 작업에서 고성능을 제공합니다.
  3. 내장된 동시성 메서드:
    • 데이터 조작 시 thread-safe를 보장하는 추가 메서드가 제공됩니다.
      • TryAdd: 키가 없으면 값을 추가합니다.
      • TryUpdate: 특정 조건에 맞는 경우 값을 업데이트합니다.
      • TryRemove: 키를 찾아 값을 제거합니다.
      • GetOrAdd: 키가 없으면 값을 추가하고, 있으면 반환합니다.
      • AddOrUpdate: 조건에 따라 추가 또는 업데이트를 처리합니다.
  4. 성능 최적화:
    • 락 경쟁을 최소화하도록 설계되었습니다.
    • 내부적으로 분할 잠금(partitioned locking)을 사용하여 병렬 작업의 효율성을 극대화합니다.

ConcurrentDictionary를 사용할 때 고려사항

  1. 읽기와 쓰기의 균형:
    • 읽기 작업이 많은 경우 ConcurrentDictionary보다 일반 Dictionary와 ReaderWriterLock을 사용하는 것이 더 효율적일 수 있습니다.
  2. 크기 제한:
    • 내부적으로 스레드 안전성을 위해 추가적인 메모리를 사용하므로 데이터 크기가 매우 큰 경우 성능에 영향을 줄 수 있습니다.
  3. 데이터 일관성:
    • TryUpdate와 같이 비교 및 업데이트를 수행하는 작업에서 데이터의 상태가 동기화되지 않으면 논리적 오류가 발생할 가능성이 있습니다.

그러면 여기서 의문점은 왜 읽기 작업이 많은 경우에는 성능이 떨어 질 수 있을까? 라는 의문이 들었습니다.

 

여기에 대해 찾아보니, 읽기 연산에서 락을 사용하지 않지만, 쓰기와의 동시 접근을 관리하기 위해 내부적으로 락 분할(lock striping)을 활용합니다.

 

그러면 쓰기가 매우 빈번한 경우, 쓰기 락에 의해 읽기 작업이 지연될 수가 있답니다.

 

그러면 만약 어떤 특정한 것을 읽기 중심으로만 사용한다고 한다면 Dictionary가 더 빠를 수 있다고 하는데,

 

결국 서버에서는 읽기만 하는 경우는 없기 때문에 ^_^.. 사용해도 무방하다는거죠.

 

class SessionManager
{
    private static readonly SessionManager _instance = new SessionManager();
    public static SessionManager Instance => _instance;

    private int _sessionId = 0;
    private readonly ConcurrentDictionary<int, ClientSession> _sessions = new ConcurrentDictionary<int, ClientSession>();

    public ClientSession Generate()
    {
        int sessionId = Interlocked.Increment(ref _sessionId);
        var session = new ClientSession
        {
            SessionId = sessionId
        };

        if (_sessions.TryAdd(sessionId, session) == false)
        {
            Log.Error($"Failed to add session. SessionId({sessionId})");
            return null;
        }

        Log.Info($"Session generated. SessionId: {sessionId}");
        return session;
    }

    public ClientSession Find(int sessionId)
    {
        _sessions.TryGetValue(sessionId, out var session);
        return session;
    }
    public void Remove(ClientSession session)
    {
        if (session == null)
        {
            Log.Error("Session is null");
            return;
        }

        if (_sessions.TryRemove(session.SessionId, out var removedSession) == false)
        {
            Log.Error($"Failed to remove session. SessionId({session.SessionId})");
            return;
        }
    }
}

저 같은 경우는 이런 식으로 사용 중이긴한데, 보통 서버에서는 Key-Value 형식으로 뭔가를 관리하면서 추가와 제거 등 해야할 게 많아서 그런지 유용한 것 같습니다.

 

lock을 없애면서 sessionId 발급에 관하여도 thread-safe 할 필요가 있었는데, 그부분은 Interlocked로 사용하였습니다.

 

아마 이 부분을 따로 다양한 곳에서 사용하고 있기 때문에 Util로 만들어 사용하지 않을까 라고 생각이 듭니다..

 

현재는 Flag 도 따로 빼서 쓰고 있거든요..

 

Interlocked의 주요 특징으로는 원자성으로 여러 스레드가 동시에 Interlocked 매서드를 호출하더라도 연산이 중단되거나 상태가 엉키지 않습니다.

 

그리고 락을 사용하는 것보다 간단하게 동기화도 가능하구요.

 

무엇보다 여러 스레드가 경쟁적으로 접근할 때 발생할 수 있는 race condition을 방지합니다.

 

즉, 스레드 간 데이터 무결성을 보장한다는 거죠 ㅎㅎ

 

이런 것들로 인하여 lock을 사용하지 않고 lock free 하게 구현하긴 했습니다. (물론 필요한 상황에선 lock을 사용해야겠지만..)

 

반응형

+ Recent posts