CancellationToken에 대한 .NET 프로그래머 가이드
게시 됨: 2022-08-23때로는 취소하는 것이 좋습니다. 많은 .NET 프로젝트에서 내부 및 외부 프로세스를 모두 취소하려는 동기가 많이 있었습니다. Microsoft는 개발자가 다양한 복잡한 구현에서 이 일반적인 사용 사례에 접근하고 있음을 알게 되었고 더 나은 방법이 있어야 한다고 결정했습니다. 따라서 일반적인 취소 통신 패턴은 낮은 수준의 멀티스레딩 및 프로세스 간 통신 구성을 사용하여 구축된 CancellationToken
으로 도입되었습니다. 이 패턴에 대한 초기 연구의 일환으로 그리고 Microsoft 구현을 위한 실제 .NET 소스 코드를 조사한 후 나는 CancellationToken
이 훨씬 더 광범위한 문제를 해결할 수 있다는 것을 발견했습니다. 트리거 및 플래그를 통한 일반 프로세스 간 통신.
의도된 CancellationToken 사용 사례
CancellationToken
은 작업 취소를 위한 기존 솔루션을 개선하고 표준화하기 위한 수단으로 .NET 4에 도입되었습니다. 인기 있는 프로그래밍 언어가 구현하는 경향이 있는 취소 처리에 대한 네 가지 일반적인 접근 방식이 있습니다.
죽이다 | 말해봐, 대답을 거절하지마 | 정중하게 묻고 거절을 받아들인다 | 플래그를 정중하게 설정하고 원하는 경우 폴링하도록 합니다. | |
---|---|---|---|---|
접근하다 | 하드 스톱; 나중에 불일치 해결 | 멈추라고 말하지만 일을 정리하게 놔두세요 | 직접적이지만 부드러운 중지 요청 | 중지를 요청하되 강요하지 마십시오. |
요약 | 부패와 고통으로 가는 확실한 길 | 깨끗한 중지 지점을 허용하지만 중지해야 합니다. | 깨끗한 중지 지점을 허용하지만 취소 요청은 무시될 수 있습니다. | 플래그를 통해 취소 요청 |
Pthread | pthread_kill ,pthread_cancel (비동기) | pthread_cancel (지연 모드) | 해당 사항 없음 | 깃발을 통해 |
.그물 | Thread.Abort | 해당 사항 없음 | Thread.Interrupt | CancellationToken 의 플래그를 통해 |
자바 | Thread.destroy ,Thread.stop | 해당 사항 없음 | Thread.interrupt | 플래그 또는 Thread.interrupted 를 통해 |
파이썬 | PyThreadState_SetAsyncExc | 해당 사항 없음 | asyncio.Task.cancel | 깃발을 통해 |
안내 | 허용되지 않음 이 접근법을 피하십시오 | 허용 가능, 특히 언어가 예외 또는 해제를 지원하지 않는 경우 | 언어가 지원하는 경우 허용 | 더 좋지만 더 많은 그룹 노력 |
CancellationToken
은 취소 대화가 협조적인 최종 범주에 있습니다.
Microsoft가 CancellationToken
을 도입한 후 개발 커뮤니티는 이를 빠르게 수용했습니다. 특히 많은 주요 .NET API가 이러한 토큰을 기본적으로 사용하도록 업데이트되었기 때문입니다. 예를 들어 ASP.NET Core 2.0부터 작업은 HTTP 요청이 닫힌 경우 신호를 보낼 수 있는 선택적 CancellationToken
매개 변수를 지원하므로 모든 작업을 취소할 수 있으므로 리소스의 불필요한 사용을 방지할 수 있습니다.
.NET 코드베이스를 자세히 살펴본 후 CancellationToken
의 사용이 취소에만 국한되지 않는다는 것이 분명해졌습니다.
현미경으로 보는 CancellationToken
CancellationToken
의 구현을 더 자세히 살펴보면 단순한 플래그(예: ManualResetEvent
)와 해당 플래그를 모니터링하고 변경할 수 있는 기능을 제공하는 지원 인프라임을 알 수 있습니다. CancellationToken
의 주요 유틸리티는 이름에 있는데, 이는 이것이 작업을 취소하는 일반적인 방법임을 시사합니다. 요즘에는 비동기 또는 장기 실행 작업이 있는 모든 .NET 라이브러리, 패키지 또는 프레임워크에서 이러한 토큰을 통해 취소할 수 있습니다.
CancellationToken
은 플래그를 수동으로 "true"로 설정하거나 특정 시간 범위가 경과한 후 "true"로 변경하도록 프로그래밍하여 트리거할 수 있습니다. CancellationToken
이 트리거되는 방식에 관계없이 이 토큰을 모니터링하는 클라이언트 코드는 다음 세 가지 방법 중 하나를 통해 토큰 플래그의 값을 결정할 수 있습니다.
-
WaitHandle
사용 -
CancellationToken
의 플래그 폴링 - 플래그 상태가 프로그래밍 방식 구독을 통해 업데이트될 때 클라이언트 코드에 알림
.NET 코드베이스에 대한 추가 연구 후 .NET 팀은 취소와 연결되지 않은 다른 시나리오에서 CancellationTokens
가 유용하다는 것을 알게 되었습니다. 복잡한 상황을 단순화하기 위해 다중 스레드 및 프로세스 간 조정을 통해 C# 개발자에게 권한을 부여하는 이러한 고급 및 브랜드 외 사용 사례 중 일부를 살펴보겠습니다.
고급 이벤트에 대한 CancellationTokens
ASP.NET Core 애플리케이션을 작성할 때 애플리케이션이 시작된 시간을 알아야 하거나 호스트 종료 프로세스에 코드를 삽입해야 하는 경우가 있습니다. 이러한 경우 IHostApplicationLifetime
인터페이스(이전에는 IApplicationLifetime
)를 사용합니다. .NET Core의 리포지토리에서 가져온 이 인터페이스는 CancellationToken
을 사용하여 ApplicationStarted
, ApplicationStopping
및 ApplicationStopped
의 세 가지 주요 이벤트를 전달합니다.
namespace Microsoft.Extensions.Hosting { /// <summary> /// Allows consumers to be notified of application lifetime events. /// This interface is not intended to be user-replaceable. /// </summary> public interface IHostApplicationLifetime { /// <summary> /// Triggered when the application host has fully started. /// </summary> CancellationToken ApplicationStarted { get; } /// <summary> /// Triggered when the application host is starting a graceful shutdown. /// Shutdown will block until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopping { get; } /// <summary> /// Triggered when the application host has completed a graceful shutdown. /// The application will not exit until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopped { get; } /// <summary> /// Requests termination of the current application. /// </summary> void StopApplication(); } }
언뜻 보기에 CancellationToken
은 여기에 속하지 않는 것처럼 보일 수 있습니다. 특히 이벤트로 사용되기 때문입니다. 그러나 추가 조사를 통해 다음과 같은 토큰이 완벽하게 적합함을 알 수 있습니다.
- 인터페이스의 클라이언트가 이러한 이벤트를 수신할 수 있는 다양한 방법을 허용하는 유연합니다.
- 그들은 상자에서 꺼낸 스레드로부터 안전합니다.
-
CancellationToken
을 결합하여 다른 소스에서 만들 수 있습니다.
CancellationToken
은 모든 이벤트 요구에 완벽하지는 않지만 애플리케이션 시작 또는 중지와 같이 한 번만 발생하는 이벤트에 이상적입니다.
시간 초과에 대한 CancellationToken
기본적으로 ASP.NET은 종료할 시간이 거의 없습니다. 시간이 조금 더 필요한 경우 내장된 HostOptions
클래스를 사용하면 이 시간 초과 값을 변경할 수 있습니다. 그 아래에서 이 시간 초과 값은 CancellationToken
에 래핑되어 기본 하위 프로세스에 제공됩니다.
IHostedService
의 StopAsync
메서드는 이 사용법의 좋은 예입니다.
namespace Microsoft.Extensions.Hosting { /// <summary> /// Defines methods for objects that are managed by the host. /// </summary> public interface IHostedService { /// <summary> /// Triggered when the application host is ready to start the service. /// </summary> /// <param name="cancellationToken">Indicates that the start /// process has been aborted.</param> Task StartAsync(CancellationToken cancellationToken); /// <summary> /// Triggered when the application host is performing a graceful shutdown. /// </summary> /// <param name="cancellationToken">Indicates that the shutdown /// process should no longer be graceful.</param> Task StopAsync(CancellationToken cancellationToken); } }
IHostedService
인터페이스 정의에서 알 수 있듯이 StopAsync
메서드는 하나의 CancellationToken
매개 변수를 사용합니다. 해당 매개변수와 관련된 주석은 CancellationToken
에 대한 Microsoft의 초기 의도가 취소 프로세스가 아니라 시간 초과 메커니즘임을 명확하게 전달합니다.
내 생각에 이 인터페이스가 CancellationToken
의 존재 이전에 존재했다면 중지 작업이 처리될 수 있는 기간을 나타내는 TimeSpan
매개변수일 수 있습니다. 내 경험에 따르면 시간 초과 시나리오는 거의 항상 훌륭한 추가 유틸리티를 사용하여 CancellationToken
으로 변환할 수 있습니다.
잠시 동안 StopAsync
메서드가 어떻게 설계되었는지 알고 있다는 사실을 잊고 대신 이 메서드의 계약을 설계하는 방법에 대해 생각합시다. 먼저 요구 사항을 정의하겠습니다.
-
StopAsync
메서드는 서비스를 중지해야 합니다. -
StopAsync
메서드에는 정상적인 중지 상태가 있어야 합니다. - 정상적인 중지 상태에 도달했는지 여부에 관계없이 호스팅된 서비스에는 timeout 매개변수에 정의된 대로 중지할 최대 시간이 있어야 합니다.
어떤 형태 StopAsync
메서드를 사용하여 첫 번째 요구 사항을 충족합니다. 나머지 요구 사항은 까다롭습니다. CancellationToken
은 표준 .NET 플래그 기반 통신 도구를 사용하여 대화를 강화함으로써 이러한 요구 사항을 정확히 충족합니다.
알림 메커니즘으로서의 CancellationToken
CancellationToken
의 가장 큰 비밀은 그것이 단지 플래그라는 것입니다. CancellationToken
을 사용하여 프로세스를 중지하는 대신 시작하는 방법을 설명하겠습니다.
다음을 고려하세요:
-
RandomWorker
클래스를 만듭니다. -
RandomWorker
에는 임의의 작업을 실행하는DoWorkAsync
메서드가 있어야 합니다. -
DoWorkAsync
메서드는 호출자가 작업을 시작해야 하는 시간을 지정할 수 있도록 해야 합니다.
public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync() { for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }
위의 클래스는 처음 두 가지 요구 사항을 충족하고 세 번째 요구 사항을 남깁니다. 시간 범위 또는 간단한 플래그와 같이 작업자를 트리거하는 데 사용할 수 있는 몇 가지 대체 인터페이스가 있습니다.
# With a time span Task DoWorkAsync(TimeSpan startAfter); # Or a simple flag bool ShouldStart { get; set; } Task DoWorkAsync();
이 두 가지 접근 방식은 괜찮지만 CancellationToken
을 사용하는 것만큼 우아한 것은 없습니다.
public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync(CancellationToken startToken) { startToken.WaitHandle.WaitOne(); for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }
이 샘플 클라이언트 코드는 이 디자인의 힘을 보여줍니다.
using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace CancelToStart { public class Program { static void Main(string[] args) { CancellationTokenSource startCts = new CancellationTokenSource(); startCts.CancelAfter(TimeSpan.FromSeconds(10)); var tasks = Enumerable.Range(0, 10) .Select(i => new RandomWorker(i)) .Select(worker => worker.DoWorkAsync(startCts.Token)) .ToArray(); Task.WaitAll(tasks, CancellationToken.None); } } }
CancellationTokenSource
는 배후에서 CancellationToken
을 만들고 모든 관련 프로세스의 트리거를 조정합니다. 이 경우 관련 프로세스는 시작을 기다리고 있는 RandomWorker
입니다. 이 접근 방식을 통해 기본 CancellationToken
구현에 포함된 스레드 안전성을 활용할 수 있습니다.
광범위한 CancellationToken 도구 상자
이러한 예는 CancellationToken
이 의도한 사용 사례 외에 유용한 솔루션의 도구 상자를 제공하는 방법을 보여줍니다. 이 도구는 프로세스 간 플래그 기반 통신과 관련된 많은 시나리오에서 유용할 수 있습니다. 시간 초과, 알림 또는 일회성 이벤트가 발생하더라도 Microsoft에서 테스트한 이 우아한 구현으로 대체할 수 있습니다.
Toptal 엔지니어링 블로그에 대한 추가 정보:
- .NET Core: 야생 및 오픈 소스로의 전환. 마이크로소프트, 왜 이렇게 오래 걸렸어?!
- ASP.NET Core를 사용하여 ASP.NET Web API 빌드
- .NET 프로젝트를 부트스트랩하고 생성하는 방법