49. 완료 카운트다운(Decrement) 종료 판정과 컨텍스트 재사용 경합 (C#)
난이도 최상해설 — 완료 카운트다운(Decrement) 종료 판정과 컨텍스트 재사용 경합 (C#)
난이도: 최상
요약
완료 판정을 Interlocked.Decrement(ref ctx.Pending) == 0으로만 하고(A), 중복 응답을 걸러내지 않는다. 신뢰성 UDP 재전송으로 이미 처리된 샤드의 응답이 한 번 더 도착하면 Pending이 추가로 감소해, (1) 이미 0인 카운터가 음수가 되며 0 전이를 다시 만들어 개더가 두 번 완성되거나, (2) 마지막 정상 응답보다 먼저 중복이 도착해 카운터가 조기에 0이 되어 아직 응답 안 온 슬롯을 비운 채 완성한다. 더 치명적으로, 0을 만든 콜백이 Return(ctx)로 컨텍스트를 풀에 반납(B)한 뒤 다른 개더가 같은 객체를 Rent 하면, 뒤늦게 도착한 옛 개더의 중복 응답이 재사용된 컨텍스트의 Pending/Results를 오염시킨다(ABA형 use-after-return).
문제점
- 분류태그: fetch-and-decrement 종료 판정 오용 / 중복 완료 미방어(dedup 부재) / 객체 풀 use-after-return(ABA)
- 증상: 검색 결과가 다른 요청 결과와 섞임, 같은 개더 이중 완성(콜백 2회), 간헐적 결과 누락.
- 재현조건: 샤드 응답 중복 도착(재전송) + 개더 완료 후 컨텍스트 풀 재사용이 겹칠 때.
- 근본원인: ① 응답 처리가 멱등하지 않다 — 슬롯이 이미 채워졌는지 확인 없이 무조건
Decrement한다. ② 컨텍스트 수명이 "마지막 감소"에 묶여 있는데, 그 이후에도 도착 가능한 늦은/중복 응답이 참조를 붙든다(유예 기간 부재).
수정안
슬롯 단위로 "처음 채워질 때만 감소"하도록 멱등화하고, 완성/반납은 한 번만 일어나도록 게이트를 둔다. 재사용은 세대(generation) 토큰으로 늦은 응답을 무시한다.
public sealed class GatherContext
{
public int Generation; // Begin 마다 증가
public int Pending;
public int[] SlotFilled; // 0/1, 슬롯별 1회 감소 보장
public int Completed; // 0/1, 완성 1회 게이트
public string?[] Results;
public Action<string?[]>? OnComplete;
}
public void OnShardResponse(GatherContext ctx, int gen, int idx, string? value)
{
if (Volatile.Read(ref ctx.Generation) != gen) return; // 재사용된 옛 응답 무시
if (Interlocked.Exchange(ref ctx.SlotFilled[idx], 1) == 1) return; // 중복 슬롯 무시
ctx.Results[idx] = value;
if (Interlocked.Decrement(ref ctx.Pending) != 0) return;
if (Interlocked.Exchange(ref ctx.Completed, 1) == 1) return; // 이중 완성 방지
ctx.OnComplete?.Invoke(ctx.Results);
Return(ctx); // Begin 에서 Generation++ 후 재사용
}
핵심 3가지: (1) 슬롯별 1회 감소(멱등), (2) 완성 1회 게이트, (3) 세대 토큰으로 재사용 후 늦은 응답 차단.
더 나은 설계
- 컨텍스트를 풀로 재사용하는 대신 참조 카운팅/유예(quiescent) 회수로 in-flight 응답이 모두 끝난 뒤 회수한다. 트레이드오프: 회수 지연·복잡도.
- 애초에 응답 경로를 **응답 dedup(요청 correlation id + 슬롯 seen 집합)**으로 멱등하게 만들면 카운트다운이 견고해진다.
- 완성 판정을 "감소로 0"이 아니라 "채워진 슬롯 수 == N"처럼 상태로부터 파생하면 중복 감소에 영향받지 않는다.
면접 포인트
if(--n==0)완료 패턴이 왜 "정확히 한 번" 도착을 전제로만 안전한지, at-least-once 환경에서 무엇을 추가해야 하는지.- 객체 풀 재사용에서 발생하는 ABA/use-after-return과 세대 토큰(generation/epoch)의 역할.
해설 — 완료 카운트다운(fetch_sub) 종료 판정과 컨텍스트 재사용 경합 (C++)
난이도: 최상
요약
완료를 pending.fetch_sub(1) == 1로만 판정하고(A) 중복 응답을 걸러내지 않는다. 재전송으로 처리 완료된 샤드 응답이 다시 도착하면 pending이 한 번 더 감소해 이중 1→0 전이가 생기고, 0을 만든 콜백이 Return(ctx)로 컨텍스트를 풀에 넣은 뒤(B) 다른 개더가 같은 포인터를 재사용하면 늦은 옛 응답이 재사용된 객체를 오염시킨다. 또한 풀에서 꺼낸 컨텍스트를 다시 delete 하거나 이중 Return 하면 use-after-free/이중 반납으로 직행한다. fetch_sub의 기본 seq_cst가 카운터의 원자성은 지키지만, "정확히 한 번 완료/수명 안전"은 카운터만으로 보장되지 않는다.
문제점
- 분류태그: fetch_sub 종료 판정 오용 / 중복 완료 미방어 / 풀 use-after-return·이중 free(ABA)
- 증상: 결과 혼선, 이중 완성 콜백, 드물게 크래시(해제된 컨텍스트 접근).
- 재현조건: 샤드 응답 중복 + 완료 후 풀 재사용이 겹칠 때.
- 근본원인: ① 응답 처리가 멱등하지 않다(슬롯 채움 여부 무검사). ② 컨텍스트 수명이 "마지막 감소"에 묶여 있으나 늦은 응답이 여전히 포인터를 참조(유예 기간 부재).
수정안
슬롯별 1회 감소(멱등)와 완성 1회 게이트, 세대 토큰으로 재사용 후 응답을 무시한다.
struct GatherContext {
std::atomic<uint32_t> generation{0};
std::atomic<int> pending{0};
std::vector<std::atomic<uint8_t>> slot_filled; // 0/1
std::atomic<uint8_t> completed{0};
std::vector<std::string> results;
std::function<void(const std::vector<std::string>&)> on_complete;
};
void OnShardResponse(GatherContext* ctx, uint32_t gen, int idx, std::string v) {
if (ctx->generation.load(std::memory_order_acquire) != gen) return; // 재사용 무시
uint8_t was = ctx->slot_filled[idx].exchange(1, std::memory_order_acq_rel);
if (was == 1) return; // 중복 슬롯 무시
ctx->results[idx] = std::move(v);
if (ctx->pending.fetch_sub(1, std::memory_order_acq_rel) != 1) return;
if (ctx->completed.exchange(1, std::memory_order_acq_rel) == 1) return; // 이중 완성 방지
ctx->on_complete(ctx->results);
Return(ctx); // Begin 에서 generation++ 후 재사용
}
- acquire/release: 결과 쓰기가 0 전이를 관측한 완성 스레드에 확실히 보이도록
fetch_sub를acq_rel로. - 멱등 슬롯·완성 게이트·세대 토큰의 3중 방어가 핵심.
더 나은 설계
- 풀 재사용 대신 epoch/hazard pointer 기반 유예 회수로 in-flight 응답이 모두 소진된 뒤 컨텍스트를 회수한다. 트레이드오프: 회수 지연·구현 복잡도.
- 응답 경로 자체를 dedup(정확히-한-번 효과)해 카운트다운이 중복에 노출되지 않게 한다.
면접 포인트
if(fetch_sub(1)==1)완료 관용구의 전제(정확히 N번 도착)와 at-least-once에서의 보강책.- 완성 스레드가 다른 스레드의 결과 쓰기를 보게 하려면 왜 release/acquire가 필요한가(가시성/happens-before).
- 객체 풀의 ABA와 세대 토큰·유예 회수.