1. Zone 통계 수집기의 데이터 레이스 (lost update / torn read)
난이도 하 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// MMORPG 필드 서버의 "구역(Zone)" 통계 수집기다.
// 한 Zone 안에는 수백 개의 몬스터 AI가 돌아가고, 각 AI는 자신을 처리하는
// 워커 스레드에서 동작한다. 워커 스레드들은 전투가 일어날 때마다
// ZoneMetrics 에 "가한 피해량(damage)"과 "처치 수(kill)"를 누적한다.
// 운영툴은 1초마다 별도 스레드에서 Snapshot() 을 읽어 대시보드에 표시한다.
//
// 요구사항
// ----------------------------------------------------------------------------
// - 여러 워커 스레드가 동시에 AddDamage / AddKill 을 호출한다.
// - 운영 스레드는 동시에 Snapshot() 으로 현재 누적값을 읽는다.
// - 통계는 "유실 없이" 정확히 누적되어야 한다 (전투 로그와 대사해 검증함).
// - 핫패스이므로 가능하면 가볍게(락 최소화) 유지하고 싶다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 멀티스레드 환경에서 어떤 결과 오류/이상이 발생하는가? 왜 발생하는가?
// 2) (A)(B)(C) 각 지점에서 무엇이 문제인지 설명하라.
// 3) 정확성을 보장하면서 핫패스 오버헤드를 최소화하도록 수정하라.
// ============================================================================
using System;
using System.Threading;
namespace ZoneStats
{
public sealed class ZoneMetrics
{
// 누적 통계
private long _totalDamage;
private int _killCount;
// 최근에 가장 큰 단일 피해를 기록(크리티컬 추적용)
private long _maxSingleHit;
// 통계가 한 번이라도 갱신되면 true. 운영툴이 "데이터 들어오기 시작했는지" 폴링.
private bool _hasData;
public void AddDamage(long amount)
{
// (A) 다수의 워커 스레드가 동시에 진입한다.
_totalDamage += amount;
// (B) 단일 최대 피해 갱신
if (amount > _maxSingleHit)
_maxSingleHit = amount;
_hasData = true;
}
public void AddKill()
{
_killCount++;
}
// 운영 스레드가 1초마다 호출
public (long damage, int kills, long maxHit) Snapshot()
{
// (C) 락 없이 세 필드를 따로따로 읽어 묶어서 반환
return (_totalDamage, _killCount, _maxSingleHit);
}
public bool HasData => _hasData;
}
// 데모용 구동 코드
public static class Demo
{
public static void Run()
{
var metrics = new ZoneMetrics();
const int workers = 8;
const int hitsPerWorker = 100_000;
var threads = new Thread[workers];
for (int t = 0; t < workers; t++)
{
threads[t] = new Thread(() =>
{
var rnd = new Random();
for (int i = 0; i < hitsPerWorker; i++)
{
metrics.AddDamage(rnd.Next(1, 1000));
if (i % 50 == 0) metrics.AddKill();
}
});
threads[t].Start();
}
// 운영 스레드 흉내
var monitor = new Thread(() =>
{
while (!metrics.HasData) Thread.Sleep(1);
for (int i = 0; i < 20; i++)
{
var s = metrics.Snapshot();
Console.WriteLine($"dmg={s.damage} kills={s.kills} max={s.maxHit}");
Thread.Sleep(10);
}
});
monitor.Start();
foreach (var th in threads) th.Join();
monitor.Join();
var final = metrics.Snapshot();
// 기대값: workers * hitsPerWorker 번의 AddDamage, kill = workers * (hitsPerWorker/50)
Console.WriteLine($"[FINAL] damage={final.damage}, kills={final.kills}");
}
}
} 결함 코드 · C++
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// MMORPG 필드 서버의 "구역(Zone)" 통계 수집기다.
// 한 Zone 안에는 수백 개의 몬스터 AI가 돌아가고, 각 AI는 자신을 처리하는
// 워커 스레드에서 동작한다. 워커 스레드들은 전투가 일어날 때마다
// ZoneMetrics 에 "가한 피해량(damage)"과 "처치 수(kill)"를 누적한다.
// 운영툴은 1초마다 별도 스레드에서 Snapshot() 을 읽어 대시보드에 표시한다.
//
// 요구사항
// ----------------------------------------------------------------------------
// - 여러 워커 스레드가 동시에 AddDamage / AddKill 을 호출한다.
// - 운영 스레드는 동시에 Snapshot() 으로 현재 누적값을 읽는다.
// - 통계는 "유실 없이" 정확히 누적되어야 한다 (전투 로그와 대사해 검증함).
// - 핫패스이므로 가능하면 가볍게(락 최소화) 유지하고 싶다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 멀티스레드 환경에서 어떤 결과 오류/이상이 발생하는가? 왜 발생하는가?
// 2) (A)(B)(C) 각 지점에서 무엇이 문제인지 설명하라.
// 3) 정확성을 보장하면서 핫패스 오버헤드를 최소화하도록 수정하라.
// ============================================================================
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <random>
#include <thread>
#include <tuple>
#include <vector>
class ZoneMetrics
{
public:
void AddDamage(std::int64_t amount)
{
// (A) 다수의 워커 스레드가 동시에 진입한다.
totalDamage_ += amount;
// (B) 단일 최대 피해 갱신
if (amount > maxSingleHit_)
maxSingleHit_ = amount;
hasData_ = true;
}
void AddKill()
{
killCount_++;
}
// 운영 스레드가 1초마다 호출
std::tuple<std::int64_t, int, std::int64_t> Snapshot() const
{
// (C) 동기화 없이 세 필드를 따로따로 읽어 묶어서 반환
return {totalDamage_, killCount_, maxSingleHit_};
}
bool HasData() const { return hasData_; }
private:
// 누적 통계
std::int64_t totalDamage_ = 0;
int killCount_ = 0;
// 최근에 가장 큰 단일 피해를 기록(크리티컬 추적용)
std::int64_t maxSingleHit_ = 0;
// 통계가 한 번이라도 갱신되면 true. 운영툴이 "데이터 들어오기 시작했는지" 폴링.
bool hasData_ = false;
};
// 데모용 구동 코드
int main()
{
ZoneMetrics metrics;
constexpr int workers = 8;
constexpr int hitsPerWorker = 100'000;
std::vector<std::thread> threads;
threads.reserve(workers);
for (int t = 0; t < workers; ++t)
{
threads.emplace_back([&metrics]() {
std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution<int> dist(1, 999);
for (int i = 0; i < hitsPerWorker; ++i)
{
metrics.AddDamage(dist(rng));
if (i % 50 == 0) metrics.AddKill();
}
});
}
// 운영 스레드 흉내
std::thread monitor([&metrics]() {
while (!metrics.HasData()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
for (int i = 0; i < 20; ++i)
{
auto [dmg, kills, maxHit] = metrics.Snapshot();
std::printf("dmg=%lld kills=%d max=%lld\n",
(long long)dmg, kills, (long long)maxHit);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
});
for (auto& th : threads) th.join();
monitor.join();
auto [dmg, kills, maxHit] = metrics.Snapshot();
// 기대값: workers * hitsPerWorker 번의 AddDamage, kill = workers * (hitsPerWorker/50)
std::printf("[FINAL] damage=%lld, kills=%d\n", (long long)dmg, kills);
return 0;
} 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.