48. 결투(듀얼) 신청 수락과 만료 타이머의 경합 (C#)
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// [문제] 필드 결투(듀얼) 신청과 수락
//
// 시나리오
// 오픈 필드에서 플레이어끼리 1:1 결투를 신청할 수 있다.
// A가 B에게 결투를 신청하면 B에게 30초짜리 신청서가 생기고,
// B가 수락하면 두 사람이 결투 상태로 들어가며 결투 인스턴스가 생성된다.
// 30초 안에 수락하지 않으면 신청서는 만료 타이머에 의해 폐기된다.
//
// 요구사항
// - 한 플레이어는 동시에 하나의 결투에만 참여할 수 있다.
// - 전투 중이거나 이동(존 이동) 중인 플레이어는 결투를 시작할 수 없다.
// - 여러 사람이 동시에 같은 사람에게 신청할 수 있고, 그중 하나만 성립해야 한다.
// - 만료와 수락이 겹쳐도 두 플레이어의 결투 상태는 항상 짝이 맞아야 한다.
//
// 과제
// (A), (B) 위치를 중심으로 이 코드가 동시 요청에서 어떤 결과를 낼 수 있는지 분석하고,
// 요구사항을 만족하도록 수정안을 제시하라.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
public enum PlayerState { Idle, InCombat, Zoning, InDuel }
public sealed class Player
{
public long Id;
public PlayerState State = PlayerState.Idle;
public long CurrentDuelId; // 0 = 없음
public readonly object Sync = new object();
}
public sealed class DuelRequest
{
public long Id;
public long FromId;
public long ToId;
public Timer ExpireTimer;
public bool Consumed; // 수락되어 소비됨
}
public sealed class Duel
{
public long Id;
public long P1, P2;
public DateTime StartedUtc;
}
public sealed class DuelService
{
private readonly ConcurrentDictionary<long, Player> _players = new();
private readonly ConcurrentDictionary<long, DuelRequest> _requests = new();
private readonly ConcurrentDictionary<long, Duel> _duels = new();
private long _nextRequestId;
private long _nextDuelId;
public DuelService(IEnumerable<Player> roster)
{
foreach (var p in roster) _players[p.Id] = p;
}
public long Request(long fromId, long toId)
{
if (fromId == toId) return 0;
if (!_players.TryGetValue(fromId, out var from)) return 0;
if (!_players.TryGetValue(toId, out var to)) return 0;
if (from.State != PlayerState.Idle || to.State != PlayerState.Idle)
return 0;
var req = new DuelRequest
{
Id = Interlocked.Increment(ref _nextRequestId),
FromId = fromId,
ToId = toId,
};
_requests[req.Id] = req;
req.ExpireTimer = new Timer(_ => Expire(req.Id), null, 30_000, Timeout.Infinite);
return req.Id;
}
// 수락자(toId)의 세션 스레드에서 호출된다.
public bool Accept(long requestId, long accepterId)
{
if (!_requests.TryGetValue(requestId, out var req)) return false;
if (req.ToId != accepterId) return false;
if (req.Consumed) return false;
var a = _players[req.FromId];
var b = _players[req.ToId];
// (A)
if (a.State != PlayerState.Idle || b.State != PlayerState.Idle)
return false;
var duel = new Duel
{
Id = Interlocked.Increment(ref _nextDuelId),
P1 = a.Id,
P2 = b.Id,
StartedUtc = DateTime.UtcNow,
};
_duels[duel.Id] = duel;
a.State = PlayerState.InDuel;
a.CurrentDuelId = duel.Id;
b.State = PlayerState.InDuel;
b.CurrentDuelId = duel.Id;
req.Consumed = true;
req.ExpireTimer.Dispose();
_requests.TryRemove(requestId, out _);
return true;
}
// 타이머 스레드풀 스레드에서 호출된다.
private void Expire(long requestId)
{
if (!_requests.TryGetValue(requestId, out var req)) return;
// (B)
if (!req.Consumed)
{
_requests.TryRemove(requestId, out _);
var a = _players[req.FromId];
var b = _players[req.ToId];
a.State = PlayerState.Idle;
a.CurrentDuelId = 0;
b.State = PlayerState.Idle;
b.CurrentDuelId = 0;
req.ExpireTimer?.Dispose();
}
}
public bool TryGetDuel(long duelId, out Duel duel) => _duels.TryGetValue(duelId, out duel);
} 결함 코드 · C++
// [문제] 필드 결투(듀얼) 신청과 수락 (C++)
//
// 시나리오
// 오픈 필드 1:1 결투. A가 B에게 신청하면 30초짜리 신청서가 만들어지고,
// B가 수락하면 결투가 시작된다. 30초가 지나면 만료 스레드가 신청서를 정리한다.
//
// 요구사항
// - 한 플레이어는 동시에 하나의 결투에만 참여한다.
// - 전투 중/존 이동 중인 플레이어는 결투를 시작할 수 없다.
// - 여러 신청이 동시에 수락돼도 하나만 성립해야 한다.
// - 만료와 수락이 겹쳐도 두 플레이어의 결투 상태는 짝이 맞아야 한다.
//
// 과제
// (A), (B) 위치를 중심으로 동시 요청에서 발생 가능한 결과를 분석하고,
// 요구사항을 만족하는 수정안을 제시하라.
#include <atomic>
#include <chrono>
#include <cstdint>
#include <memory>
#include <mutex>
#include <thread>
#include <unordered_map>
enum class PlayerState { Idle, InCombat, Zoning, InDuel };
struct Player {
std::uint64_t id = 0;
PlayerState state = PlayerState::Idle;
std::uint64_t currentDuelId = 0;
std::mutex sync;
};
struct DuelRequest {
std::uint64_t id = 0;
std::uint64_t fromId = 0;
std::uint64_t toId = 0;
bool consumed = false;
};
struct Duel {
std::uint64_t id = 0;
std::uint64_t p1 = 0, p2 = 0;
};
class DuelService {
public:
explicit DuelService(std::unordered_map<std::uint64_t, std::unique_ptr<Player>> roster)
: players_(std::move(roster)) {}
std::uint64_t Request(std::uint64_t fromId, std::uint64_t toId) {
if (fromId == toId) return 0;
Player* a = Find(fromId);
Player* b = Find(toId);
if (!a || !b) return 0;
if (a->state != PlayerState::Idle || b->state != PlayerState::Idle) return 0;
auto* req = new DuelRequest{++nextRequestId_, fromId, toId, false};
{
std::lock_guard<std::mutex> lk(reqMx_);
requests_[req->id] = req;
}
std::thread([this, id = req->id] {
std::this_thread::sleep_for(std::chrono::seconds(30));
Expire(id);
}).detach();
return req->id;
}
// 수락자의 세션 스레드에서 호출된다.
bool Accept(std::uint64_t requestId, std::uint64_t accepterId) {
DuelRequest* req = nullptr;
{
std::lock_guard<std::mutex> lk(reqMx_);
auto it = requests_.find(requestId);
if (it == requests_.end()) return false;
req = it->second;
}
if (req->toId != accepterId) return false;
if (req->consumed) return false;
Player* a = Find(req->fromId);
Player* b = Find(req->toId);
if (!a || !b) return false;
// (A)
std::lock_guard<std::mutex> la(a->sync);
std::lock_guard<std::mutex> lb(b->sync);
if (a->state != PlayerState::Idle || b->state != PlayerState::Idle) return false;
auto duel = std::make_shared<Duel>();
duel->id = ++nextDuelId_;
duel->p1 = a->id;
duel->p2 = b->id;
{
std::lock_guard<std::mutex> lk(duelMx_);
duels_[duel->id] = duel;
}
a->state = PlayerState::InDuel;
a->currentDuelId = duel->id;
b->state = PlayerState::InDuel;
b->currentDuelId = duel->id;
req->consumed = true;
{
std::lock_guard<std::mutex> lk(reqMx_);
requests_.erase(requestId);
}
delete req;
return true;
}
private:
void Expire(std::uint64_t requestId) {
DuelRequest* req = nullptr;
{
std::lock_guard<std::mutex> lk(reqMx_);
auto it = requests_.find(requestId);
if (it == requests_.end()) return;
req = it->second;
}
// (B)
if (!req->consumed) {
{
std::lock_guard<std::mutex> lk(reqMx_);
requests_.erase(requestId);
}
Player* a = Find(req->fromId);
Player* b = Find(req->toId);
if (a) { a->state = PlayerState::Idle; a->currentDuelId = 0; }
if (b) { b->state = PlayerState::Idle; b->currentDuelId = 0; }
delete req;
}
}
Player* Find(std::uint64_t id) {
auto it = players_.find(id);
return it == players_.end() ? nullptr : it->second.get();
}
std::unordered_map<std::uint64_t, std::unique_ptr<Player>> players_;
std::unordered_map<std::uint64_t, DuelRequest*> requests_;
std::unordered_map<std::uint64_t, std::shared_ptr<Duel>> duels_;
std::mutex reqMx_, duelMx_;
std::atomic<std::uint64_t> nextRequestId_{0};
std::atomic<std::uint64_t> nextDuelId_{0};
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.