27. 전역 락을 쥔 채 하는 블로킹 I/O와 재진입 콜백 (락 호위 / 순회 중 변경)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 전역 락을 쥔 채 수행하는 블로킹 전송과 실패 시 연결 정리
// ----------------------------------------------------------------------------
// 시나리오 (동시성/메모리 - 서버 내부):
// - WorldManager 는 전역 락(_lock)으로 세션 목록과 월드 상태를 보호한다.
// - Broadcast 는 모든 세션에 같은 패킷을 보낸다(존 알림, 채팅, 틱 스냅샷 등).
// - SendBlocking 은 소켓 송신이며, 수신이 느리거나 송신 버퍼가 가득 찬 클라이언트에서는
// 한참 블로킹될 수 있다.
// - 전송이 실패하면 그 세션을 Disconnect 로 정리(목록에서 제거 + 소켓 닫기)한다.
// - AddSession/Disconnect 는 다른 스레드에서도 호출된다(접속/해제).
//
// 상황:
// - 동접이 많은 상태에서 Broadcast 가 자주 일어난다.
// - 일부 클라이언트가 느리고(백프레셔), 그중 하나는 전송 중 끊겨 예외를 던진다.
//
// 과제:
// (A)에서 전역 락을 잡은 채 (B)의 블로킹 전송을 도는 동안, 다른 스레드의 AddSession/
// Disconnect/다른 Broadcast 가 어떻게 되는지 분석하라. 또 (B)가 실패해 (C)의 Disconnect
// 가 (D)에서 같은 락을 다시 잡고 목록을 바꿀 때, Broadcast 의 순회와 락 상태가 어떻게
// 되는지 설명하라. 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System.Collections.Generic;
public sealed class Session
{
public long Id;
public void SendBlocking(byte[] packet) { /* 소켓 송신: 느린 클라에서 블로킹/예외 가능 */ }
public void Close() { /* 소켓 닫기 */ }
}
public sealed class WorldManager
{
private readonly object _lock = new();
private readonly List<Session> _sessions = new();
public void AddSession(Session s)
{
lock (_lock) { _sessions.Add(s); }
}
public void Broadcast(byte[] packet)
{
lock (_lock) // (A) 전역 락 획득
{
foreach (var s in _sessions)
{
try
{
s.SendBlocking(packet); // (B) 락 보유 중 블로킹 전송
}
catch
{
Disconnect(s); // (C) 실패한 세션 정리
}
}
}
}
public void Disconnect(Session s)
{
lock (_lock) // (D) 같은 락 재획득 후 목록 변경
{
_sessions.Remove(s);
s.Close();
}
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 전역 락을 쥔 채 수행하는 블로킹 전송과 실패 시 연결 정리
// ----------------------------------------------------------------------------
// 시나리오 (동시성/메모리 - 서버 내부):
// - WorldManager 는 전역 뮤텍스(mtx_)로 세션 목록과 월드 상태를 보호한다.
// - broadcast 는 모든 세션에 같은 패킷을 보낸다(존 알림, 채팅, 틱 스냅샷 등).
// - sendBlocking 은 소켓 송신이며, 수신이 느리거나 송신 버퍼가 가득 찬 클라이언트에서는
// 한참 블로킹될 수 있고, 끊긴 연결에서는 예외를 던진다.
// - 전송이 실패하면 그 세션을 disconnect 로 정리(목록에서 제거 + 소켓 닫기)한다.
// - addSession/disconnect 는 다른 스레드에서도 호출된다(접속/해제).
//
// 상황:
// - 동접이 많은 상태에서 broadcast 가 자주 일어난다.
// - 일부 클라이언트가 느리고(백프레셔), 그중 하나는 전송 중 끊겨 예외를 던진다.
//
// 과제:
// (A)에서 전역 뮤텍스를 잡은 채 (B)의 블로킹 전송을 도는 동안, 다른 스레드의 addSession/
// disconnect/다른 broadcast 가 어떻게 되는지 분석하라. 또 (B)가 실패해 (C)의 disconnect
// 가 (D)에서 같은 뮤텍스를 다시 잠그려 할 때 무슨 일이 일어나는지(std::mutex 의 성질)
// 설명하라. 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <vector>
#include <memory>
#include <mutex>
#include <algorithm>
struct Session {
long id = 0;
void sendBlocking(const std::vector<uint8_t>& packet); // 느린 클라에서 블로킹/예외 가능
void close();
};
class WorldManager {
public:
void addSession(const std::shared_ptr<Session>& s) {
std::lock_guard<std::mutex> lk(mtx_);
sessions_.push_back(s);
}
void broadcast(const std::vector<uint8_t>& packet) {
std::lock_guard<std::mutex> lk(mtx_); // (A) 전역 락 획득
for (auto& s : sessions_) {
try {
s->sendBlocking(packet); // (B) 락 보유 중 블로킹 전송
} catch (...) {
disconnect(s); // (C) 실패한 세션 정리
}
}
}
void disconnect(const std::shared_ptr<Session>& s) {
std::lock_guard<std::mutex> lk(mtx_); // (D) 같은 뮤텍스 재잠금 후 목록 변경
sessions_.erase(std::remove(sessions_.begin(), sessions_.end(), s), sessions_.end());
s->close();
}
private:
std::vector<std::shared_ptr<Session>> sessions_;
std::mutex mtx_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.