C.W.K.
Stream
Lesson 05 of 05 · published

산 자를 취소한 유령

~12 min · race-condition, generation-token, state-machine, async

Level 0불씨 없음
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"'진행 중인 녹음 없음' — 앱이 말했어, 녹음이 진행 중인 동안."

기능과 크래시

탭 문법은 힘 안 들게 하려던 거야: 홀드 로 눌러서-말하기; 빠른 탭 다음 두 번째 탭으로 핸즈프리 토글. 첫 구현은 뻔했고 틀렸어: 빠른 탭이 세션을 취소하고, 두 번째 탭이 새 핸즈프리를 시작. 가끔 됐어. 가끔 '진행 중인 녹음 없음' 으로 죽었어 — 찍히는 그 순간에, 사실상 거짓인 에러 메시지.

레이스

첫 탭이 async 취소를 쏴. 두 번째 탭이 새 세션을 시작. 취소가 느리면 — 가끔 느려, 오디오 무너뜨리기가 즉각적이지 않으니까 — 새 세션이 존재한 도착하고, 자기가 낡았는지 모르고, 엉뚱한 녹음을 무너뜨려. 이미 죽은 세션의 유령이 손을 뻗어 산 걸 죽여. 그다음 세션을 기대한 코드가 아무것도 못 찾고, 그 어이없음을 신고해.

탭 1  -> 세션 A 취소  (async, 느림...)
탭 2  -> 세션 B 시작   (빠름)
           ... A-취소가 드디어 착륙 ...
           -> B 를 무너뜨림  # 유령이 엉뚱한 세션을 죽임
           -> "진행 중인 녹음 없음"

해법 둘, 둘 다 필요

첫 해법이 레이스를 통째로 없애: 세션을 절대 재시작 안 함. 빠른 탭이 더는 취소 안 해 — 짧은 결정 창(약 0.45초) 동안 세션을 녹음 중으로 둬. 두 번째 탭이 오면, 같은 세션이 제자리에서 핸즈프리로 변환; 창이 두 번째 탭 없이 만료되면, 그때 취소. 유령이 착륙할 무너뜨리기-다음-시작 틈이 없어. 두 번째 해법은 심층 방어: 모든 오디오 세션에 세대 토큰 을 찍고, 어떤 취소든 작동 전에 자기 세대를 확인하게 해. 세대 1 의 취소가 세대 2 에 도착하면 그냥 no-op 이야. 해법 하나가 레이스를 없애고; 해법 둘이 레이스가 다시 나타나도 피해를 못 주게 보장해.

무너뜨리고 다시 짓는 대신 제자리에서 변환해. 상태 변화를 파괴하고 대체물을 만드는 대신 기존 걸 변형하는 걸로 표현할 수 있을 때마다, 그걸 선호해 — 파괴-생성 틈이 모든 낡은-async 버그가 사는 곳이야. 틈 없으면, 유령도 없어.
async 작업에 세대를 찍고, 도착 때 확인해. 자기를 낳은 것보다 오래 살 수 있는 취소, 콜백, 완료는 작동 전에 자기가 아직 유효한지 증명해야 해. 자원에 단조 증가하는 세대 토큰을 두고, 효과 순간에 비교하면, '낡은 작업이 현재 상태를 망침' 을 '낡은 작업이 무해하게 no-op' 으로 바꿔. 몇 줄이고 버그 부류 하나를 통째로 죽여.

Code

제자리 변환; 낡은 세대는 no-op 만 가능·swift
actor AudioSession {
    private var generation = 0

    func start() -> Int {
        generation += 1
        return generation          // 호출자가 자기 세대 토큰을 쥠
    }

    func cancel(generation g: Int) {
        guard g == generation else { return }   // 낡은 유령 -> no-op
        teardown()
    }
}

// 빠른 탭은 취소 안 함. 같은 세션에 결정 창을 엶.
func onQuickTap() {
    phase = .recording(pendingDecision: true)
    decisionTask = Task {
        try? await Task.sleep(for: .milliseconds(450))
        guard !Task.isCancelled else { return }
        await stopAndCancel()          // 창 만료, 두 번째 탭 없음
    }
}

func onSecondTap() {
    decisionTask?.cancel()
    phase = .recording(handsFree: true)   // 제자리 변환 — 절대 재시작 안 함
}

External links

Exercise

취소, 타임아웃, 콜백이 타깃이 사라지고 대체물이 존재한 후 도착할 수 있는 async 코드를 찾아. 유령을 추적해: 늦은 도착이 새 객체한테 뭘 해? 그다음 세대 토큰을 더하고 같은 순서가 이제 어떻게 무해하게 흘러가는지 묘사해.
Hint
유령 패턴은 늘: A 의 느린 무너뜨리기가 B 의 빠른 생성 후 착륙하고, 무너뜨리기는 B 가 A 가 아님을 알 방법이 없어. 세대 토큰이 있으면, 늦은 취소가 자기 도장을 현재 것과 비교해, 불일치를 보고, 아무것도 안 건드리고 돌아가. 순서는 여전히 벌어져 — 그냥 더는 중요하지 않을 뿐.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.