4. C# 비동기 소켓 종료 처리 순서 (in-flight async 와 Dispose)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 비동기 소켓 종료 처리 순서 (in-flight async 와 Dispose)
// ----------------------------------------------------------------------------
// 시나리오:
// - 비동기 I/O(SocketAsyncEventArgs / async-await). 한 세션에 대해 여러 개의
// 비동기 작업이 동시에 in-flight 상태일 수 있다: ReceiveAsync 완료,
// SendAsync 완료, 그리고 종료(Close).
// - 완료 콜백/continuation 들은 스레드 풀의 여러 스레드에서 실행된다. 같은
// 세션의 서로 다른 완료가 서로 다른 스레드에서 거의 동시에 올라올 수 있다.
// - Close 도중에도 이미 커널에 걸려 있던 Receive 완료가 뒤늦게 올라온다.
//
// 요구사항:
// - 종료 후 콜백이 Dispose 된 객체를 건드리면 안 된다(ObjectDisposedException 금지).
// - 소켓/버퍼는 정확히 한 번만 해제되어야 한다(이중 Dispose 금지).
// - 모든 in-flight I/O 가 끝난 뒤에 자원을 해제해야 한다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Net.Sockets;
using System.Threading.Tasks;
public sealed class Connection : IDisposable
{
private readonly Socket _socket;
private readonly byte[] _recvBuf = new byte[4096]; // (A)
private bool _closed = false; // (B)
private int _pendingIo = 0; // in-flight 비동기 작업 수
public Connection(Socket socket) { _socket = socket; }
public void PostRecv()
{
_pendingIo++;
// 비동기 수신 시작 (continuation 으로 OnRecvComplete)
_ = ReceiveLoopAsync();
}
// (C) Receive 완료 — 스레드 풀에서 실행
private async Task ReceiveLoopAsync()
{
int bytes = await _socket.ReceiveAsync(_recvBuf, SocketFlags.None);
_pendingIo--;
if (bytes <= 0) // 상대가 닫음 / 에러
{
Close();
return;
}
ProcessPacket(_recvBuf, bytes);
PostRecv(); // 다음 수신 재등록
}
public async Task SendAsync(byte[] data)
{
_pendingIo++;
await _socket.SendAsync(data, SocketFlags.None);
_pendingIo--;
}
// (D) 종료 — 어느 스레드에서든 호출될 수 있음
public void Close()
{
if (_closed) return;
_closed = true;
Dispose(); // (E) 즉시 자원 해제
}
public void Dispose()
{
_socket.Dispose(); // (F) in-flight Receive/Send 가 남아있어도 즉시 Dispose
}
private void ProcessPacket(byte[] buf, int n) { /* ... */ }
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - IOCP/epoll 스타일 비동기 소켓 종료 처리 순서
// ----------------------------------------------------------------------------
// 시나리오:
// - 비동기 I/O(완료 기반). 한 세션에 대해 여러 개의 비동기 작업이 동시에
// in-flight 상태일 수 있다: Recv 완료, Send 완료, 그리고 종료(Close).
// - 완료 콜백들은 IO 스레드 풀(N개)에서 호출된다. 같은 세션의 서로 다른
// 완료가 서로 다른 스레드에서 거의 동시에 올라올 수 있다.
// - Close 도중에도 이미 커널에 걸려 있던 Recv 완료가 뒤늦게 올라온다.
//
// 요구사항:
// - 종료 후 콜백이 죽은 세션 메모리를 건드리면 안 된다(use-after-free 금지).
// - 버퍼/세션은 정확히 한 번만 해제되어야 한다(double-free 금지).
// - 모든 in-flight I/O 가 끝난 뒤에 세션을 해제해야 한다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <atomic>
#include <vector>
#include <cstdint>
#include <cstring>
struct Buffer { char* data; size_t cap; };
class Connection {
public:
int fd;
Buffer recvBuf;
bool closed = false; // (A)
std::atomic<int> pendingIo{0}; // in-flight 비동기 작업 수
Connection(int f) : fd(f) {
recvBuf.data = new char[4096];
recvBuf.cap = 4096;
}
~Connection() {
delete[] recvBuf.data; // (B)
}
void PostRecv() {
pendingIo.fetch_add(1);
// 커널에 비동기 Recv 등록 ... (생략)
}
// (C) Recv 완료 콜백 — IO 스레드에서 호출
void OnRecvComplete(int bytes) {
pendingIo.fetch_sub(1);
if (bytes <= 0) { // 상대가 닫음 / 에러
Close();
return;
}
// 수신 데이터 처리
ProcessPacket(recvBuf.data, bytes);
PostRecv(); // 다음 수신 재등록
}
void OnSendComplete(int bytes) {
pendingIo.fetch_sub(1);
// ... 송신 완료 처리
}
// (D) 종료 — 어느 스레드에서든 호출될 수 있음
void Close() {
if (closed) return;
closed = true;
// 소켓 닫기
// close(fd);
delete this; // (E)
}
void ProcessPacket(const char* p, int n) { /* ... */ }
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.