어쩌다보니, 아직도 C# 서버로 포트폴리오 작업을 하고 있는데요.
클라이언트 작업과 서버 작업을 동시에 하다 보니, 생각보다 오래 걸립니다.
계속해서 기능을 추가하다보니, 오래 걸리네요..
일단 이번에 서버를 만들면서 어떻게 Lock Free 하게 만들 수 있을까 보다 보니,
C# 에도 동일한 Concurrent 와 Interlocked가 있더라구요.
주로 사용하는 ConcurrentDictionary 와 Interlocked 에 대해 간단히 정리합니다.
ConcurrentDictionary
System.Collections.Concurrent 네임스페이스에 포함된 스레드-안전(thread-safe) 컬렉션입니다.
기본적으로 여러 스레드가 동시에 읽기 및 쓰기를 시도하더라도 데이터 무결성을 유지하도록 설계되어 있답니다.
주요 특징
- 스레드 안전성 (Thread-Safety):
- 여러 스레드가 동시에 데이터를 추가, 수정, 삭제해도 동기화 문제가 발생하지 않습니다.
- 내부적으로 가벼운 락이나 락 없는 알고리즘을 사용하여 동시 접근을 처리합니다.
- 키-값 쌍 관리:
- 키와 값을 기반으로 데이터를 저장하며, 기존 Dictionary와 같은 방식으로 작동합니다.
- 데이터 추가, 업데이트, 삭제, 조회 등의 작업에서 고성능을 제공합니다.
- 내장된 동시성 메서드:
- 데이터 조작 시 thread-safe를 보장하는 추가 메서드가 제공됩니다.
- TryAdd: 키가 없으면 값을 추가합니다.
- TryUpdate: 특정 조건에 맞는 경우 값을 업데이트합니다.
- TryRemove: 키를 찾아 값을 제거합니다.
- GetOrAdd: 키가 없으면 값을 추가하고, 있으면 반환합니다.
- AddOrUpdate: 조건에 따라 추가 또는 업데이트를 처리합니다.
- 데이터 조작 시 thread-safe를 보장하는 추가 메서드가 제공됩니다.
- 성능 최적화:
- 락 경쟁을 최소화하도록 설계되었습니다.
- 내부적으로 분할 잠금(partitioned locking)을 사용하여 병렬 작업의 효율성을 극대화합니다.
ConcurrentDictionary를 사용할 때 고려사항
- 읽기와 쓰기의 균형:
- 읽기 작업이 많은 경우 ConcurrentDictionary보다 일반 Dictionary와 ReaderWriterLock을 사용하는 것이 더 효율적일 수 있습니다.
- 크기 제한:
- 내부적으로 스레드 안전성을 위해 추가적인 메모리를 사용하므로 데이터 크기가 매우 큰 경우 성능에 영향을 줄 수 있습니다.
- 데이터 일관성:
- 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을 사용해야겠지만..)
'C#' 카테고리의 다른 글
[ASP.NET CORE] Unity와 Protobuf로 통신하기 (0) | 2024.12.24 |
---|---|
[ASP.NET Core] Service, Repository 패턴 적용하기 (1) | 2024.12.22 |
[C#] 인터페이스와 추상 클래스 (1) | 2024.11.18 |
[C#] Csv 파일을 Json 파일로 바꾸기 (3) | 2024.11.14 |
[C#] Unity에서 ProtoBuf 사용하는 방법 (1) | 2024.11.04 |