29. ConcurrentDictionary.GetOrAdd의 팩토리 중복 호출
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [언어 내부동작 문제] C# - ConcurrentDictionary.GetOrAdd의 팩토리 중복 호출
// ----------------------------------------------------------------------------
// 주제: ConcurrentDictionary 는 스레드 안전한데, 왜 GetOrAdd 에 넘긴 값 생성 델리게이트
// (factory)가 "한 번보다 더 많이" 호출될 수 있는가?
// - 게임서버에서 흔한 패턴: 플레이어 ID -> 세션/캐시 객체를 여러 스레드(IOCP 워커,
// 매치메이킹 스레드 등)가 동시에 GetOrAdd 로 최초 생성하며 접근한다.
//
// 과제:
// 1) 두 스레드가 동시에 같은 키로 GetOrAdd 를 호출할 때 아래 코드의 출력이
// 어떻게 나올 수 있는지 예측하라(팩토리가 몇 번 호출될 수 있는지, 최종적으로
// 딕셔너리에 남는 값과 두 스레드가 "받는" 값이 같은지 다른지).
// 2) ConcurrentDictionary.GetOrAdd(key, Func<TKey,TValue> factory) 가 내부적으로
// "락을 잡고 factory 호출까지 통째로 보호"하지 않는 이유(락 경합을 줄이기 위한
// 설계)를 설명하라.
// 3) factory 가 부수효과(리소스 할당, 카운터 증가, 로그 출력 등)를 갖고 있을 때
// 왜 위험한지, 안전하게 쓰려면 어떻게 바꿔야 하는지 설명하라.
// (먼저 직접 예측·설명을 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
public class SessionCache
{
private readonly ConcurrentDictionary<int, PlayerSession> _sessions = new();
private int _factoryCallCount = 0;
public class PlayerSession
{
public int PlayerId { get; }
public Guid InstanceTag { get; } = Guid.NewGuid();
public PlayerSession(int playerId)
{
PlayerId = playerId;
}
}
public PlayerSession GetOrCreate(int playerId)
{
return _sessions.GetOrAdd(playerId, id =>
{
// (A) 이 팩토리 델리게이트는 "최종적으로 딕셔너리에 들어가는 값"을 만들 때만
// 호출되는 게 아닐 수 있다.
Interlocked.Increment(ref _factoryCallCount);
return new PlayerSession(id);
});
}
public int FactoryCallCount => _factoryCallCount;
}
public static class Program
{
public static void Main()
{
var cache = new SessionCache();
const int playerId = 100;
const int threadCount = 8;
var barrier = new Barrier(threadCount);
var results = new SessionCache.PlayerSession[threadCount];
var tasks = new Task[threadCount];
for (int i = 0; i < threadCount; i++)
{
int idx = i;
tasks[idx] = Task.Run(() =>
{
barrier.SignalAndWait(); // 최대한 동시에 GetOrCreate 호출을 시도
results[idx] = cache.GetOrCreate(playerId);
});
}
Task.WaitAll(tasks);
// (B) 모든 스레드가 "같은" PlayerSession 인스턴스를 받았는지 확인
bool allSame = true;
for (int i = 1; i < threadCount; i++)
{
if (!ReferenceEquals(results[0], results[i]))
allSame = false;
}
Console.WriteLine($"factory 호출 횟수 >= 1: {cache.FactoryCallCount >= 1}");
Console.WriteLine($"모든 스레드가 동일 인스턴스를 참조: {allSame}");
Console.WriteLine($"딕셔너리 최종 원소 개수: 1 (키가 하나이므로)");
}
} 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.