비동기 작업의 취소
개요
비동기 작업의 취소(cancellation)는 호출자가 더 이상 필요하지 않은 작업에 중단 의사를 전달하고, 그 작업이 종료되거나 결과가 더 사용되지 않도록 수명을 조정하는 일이다. 화면을 닫았는데도 이미지 다운로드가 이어지거나, 새 검색을 시작했는데 옛 검색 결과가 나중에 화면을 덮을 때 취소가 필요해진다.
중단하려는 대상에는 층이 여럿이다. 결과를 기다리던 코드를 끝내는 일, 계산이나 통신을 실제로 멈추는 일, 이미 끝난 작업의 결과를 적용하지 않는 일은 서로 다른 동작이고, 어느 층을 취소하는지는 API의 계약을 읽어 확인해야 한다.
작업을 시작한 뒤에는 호출자가 그 내부의 모든 실행을 직접 통제하기 어렵다. 네트워크 요청은 원격 서버로 넘어가고, 파일 읽기는 운영체제의 입출력과 연결되며, 계산은 이벤트 루프를 점유할 수 있다. 그래서 많은 API가 협력적 취소를 쓴다. 호출자는 취소 신호를 전달하고, 작업은 그 신호를 관찰해 스스로 정리한다.
취소에는 작업 수명과 함께 동시성 제어 문제도 걸려 있다. 새 검색의 시작과 옛 검색의 완료가 어떤 순서로 처리되느냐에 따라 화면이 달라질 수 있어서, 작업을 멈추려는 시도와 화면을 바꿀 권한의 관리를 함께 봐야 이 문제의 구조가 보인다.
연혁
2009년 5월: Mike Liddell이 Microsoft .NET Blog의 「.NET 4 Cancellation Framework」에서 중단을 요청하는
CancellationTokenSource와 작업에 전달하는CancellationToken을 나눈 .NET 4의 취소 설계를 소개했다. Parallel Extensions의 타입 전반에 이 방식을 적용했다고 밝힌 당시 설계 소개 글이다.12010년 4월:
CancellationTokenSource와CancellationToken이 .NET Framework 4에 들어갔다.232015년 3월: WHATWG fetch 저장소에 「Aborting a fetch」 이슈 #27이 개설돼 취소 인터페이스를 논의했다.45
2015년 6월: ECMAScript 2015(ECMA-262 제6판)가 Promise를 JavaScript 표준에 넣었다.6
2017년: 브라우저가 fetch 취소를 지원하기 시작했다. Jake Archibald의 「Abortable fetch」는 2015년 논의부터 이 과정을 기록하며, 글에 붙은 정정에 따르면 Edge 16이 먼저 지원했다.4
강제 중단과 협력적 취소
Mike Liddell은 .NET 4 취소 프레임워크를 소개하는 글에서 실행 중인 코드를 강제로 끊는 접근과 중단을 요청하는 접근을 비교했다. 강제로 끊으면 공유 데이터나 자원이 정리되지 않은 지점에서 코드가 멈출 수 있다. 협력적 취소에서는 작업이 중단 요청을 관찰하고 자신이 맡은 정리를 직접 수행한다.1
같은 글에 따르면 기존 API는 중단 방법이 객체마다 달랐다. 작업 객체의 취소 메서드나 플래그에 기대면 요청 하나를 여러 작업에 전달하기 어렵고, 여러 작업 가운데 특정 실행만 구별하기도 어렵다. 그래서 작업을 수행하는 객체와 실행 한 번의 수명을 분리할 필요가 생긴다.
.NET은 이 분리를 두 타입으로 나타낸다. 호출자는 CancellationTokenSource로 중단 의사를 전달하고, 작업은 전달받은 CancellationToken을 확인하거나 거기에 콜백을 등록한다. 같은 요청을 여러 작업이 관찰할 수도 있다.1 이렇게 나누면 취소 권한을 가진 쪽과 그 요청에 반응하는 쪽이 코드에서 구별된다. 현재 .NET 문서도 요청, 관찰, 작업별 정리를 협력적 취소의 절차로 설명하며, 요청을 보낸 시각과 작업이 실제로 끝난 시각 사이에는 간격이 있다.7
결과를 기다리는 일과 실행을 멈추는 일
Promise는 아직 정해지지 않은 결과와 그 뒤의 성공·실패 반응을 표현하며, pending에서 fulfilled 또는 rejected로 정해진다.6 하나의 결과를 여러 코드가 기다릴 수 있고, 결과의 생산자가 통신을 하는지 계산을 하는지는 Promise만으로 정해지지 않는다. 그래서 결과를 기다리는 인터페이스와 생산자의 실행 수명을 제어하는 인터페이스가 따로 필요하다.
타이머와 요청 가운데 먼저 정해진 Promise를 고르면 기다림에 기한을 둘 수 있지만, 남은 요청의 자원 사용이나 원격 서버의 처리까지 끝내려면 그 작업이 지원하는 중단 수단을 함께 연결해야 한다.
취소 신호와 결과 적용 권한
웹 플랫폼에서는 DOM의 AbortController와 AbortSignal이 요청자와 관찰자를 나눈다. controller가 중단을 요청하면 signal에 중단 상태와 이유가 남고, signal을 받은 API가 자신의 실행을 어떻게 중단할지 정한다. 두 타입이 맡은 역할은 앞서 본 .NET의 요청자·관찰자 구분과 비교해 볼 수 있다.8
현재 DOM 표준은 진행 중인 활동의 중단을 위한 공통 신호를 정의하고, Fetch 표준은 이 신호를 요청과 응답 본문에 연결한다. 응답 객체를 받은 뒤에도 본문 읽기는 계속될 수 있으므로, 응답 도착과 본문 처리 완료는 서로 다른 시점이다.89
협력적 취소는 요청, 관찰, 정리, 완료 순으로 진행된다. 긴 동기 계산은 신호 객체를 넘겨받는 것만으로는 멈추지 않는다. 계산 도중에 신호를 관찰할 지점이 있어야 하고, 다른 코드가 실행될 기회도 있어야 한다.
외부 쓰기는 수명이 따로 있다. 클라이언트가 통신을 중단했을 때 서버가 이미 커밋했다면, 그 변경이 어떻게 처리됐는지는 서버 조회나 별도의 취소 명령 같은 애플리케이션 계약으로 확인한다.
검색 A 다음에 B를 시작해도 A가 더 늦게 끝날 수 있고, 완료 순서대로 화면에 적용하면 오래된 A의 결과가 최종 화면에 남는다. React 공식 문서의 Effect 정리 예제는 오래된 응답을 무시하는 방식으로 이 경쟁을 설명한다.10
여기서는 작업을 줄이는 중단 요청과 현재 화면을 바꿀 권한을 함께 관리한다. 요청 번호나 세대로 최신 요청을 구별해 두면 성공 결과는 물론 오류 표시와 로딩 해제에도 같은 규칙을 적용할 수 있다. 부모 기능이 끝날 때 하위 작업을 어디까지 끝낼지도 수명 계약에 들어간다.
취소 상태를 읽는 예제
다음 코드는 통신이나 비동기 계산 없이 중단 요청 전후의 signal 상태만 읽어서, controller와 signal의 역할을 확인한다.
function inspect(signal: AbortSignal): string {
return signal.aborted ? 'cancelled' : 'ready';
}
const controller = new AbortController();
console.log(inspect(controller.signal));
controller.abort();
console.log(inspect(controller.signal));
출력은 ready, cancelled 순이다. abort()가 signal의 상태를 바꾸고 inspect가 그 상태를 읽는다. 실제 작업을 멈추는 예제로 확장하려면 작업 도중의 관찰 지점이나 취소를 지원하는 API가 필요하다.
각주
- Mike Liddell, 「.NET 4 Cancellation Framework」, Microsoft .NET Blog, 2009-05-22. 당시 설계 문제와 token/source 분리에 대한 소개 글. 원문. ↩
- Microsoft, 「Cancellation」(.NET Framework 4 보관 문서). .NET Framework 4가 CancellationTokenSource와 CancellationToken을 중심으로 한 통합 협력적 취소 모델을 도입했다는 설명. 공식 문서. ↩
- Microsoft, 「Microsoft .NET Framework - Microsoft Lifecycle」. .NET Framework 4.0 지원 시작일 2010-04-12. 공식 문서. ↩
- Jake Archibald, 「Abortable fetch」, 2017, 본문 및 “The history”. Chrome 개발자 블로그 게재본. ↩
- WHATWG, whatwg/fetch 이슈 #27 「Aborting a fetch」, 2015-03-26 개설. 이슈 본문에는 앞선 논의의 링크도 있다. 이슈. ↩
- ECMA-262 제6판, 2015, §25.4 “Promise Objects”. 당시 표준. ↩
- Microsoft, 「Cancellation in Managed Threads」. 요청·관찰·정리로 이어지는 현재 .NET 취소 계약. 공식 문서. ↩
- WHATWG DOM, §3 “Aborting ongoing activities”. 표준. ↩
- WHATWG Fetch,
fetch()와 abort 알고리즘. 표준. ↩ - React, “Fetching data with Effects”. 공식 문서. ↩