Visual Studio에서 C# 및 Visual Basic 컴파일러를 수동으로 사용하거나 컴파일러와 수동 메서드의 조합을 통해 세 가지 방법으로 TAP(작업 기반 비동기 패턴)를 구현할 수 있습니다. 다음 섹션에서는 각 메서드에 대해 자세히 설명합니다. TAP 패턴을 사용하여 컴퓨팅 바인딩 및 I/O 바인딩된 비동기 작업을 모두 구현할 수 있습니다. 워크로드 섹션에서는 각 작업 유형에 대해 설명합니다.
TAP 메서드 생성
컴파일러 사용
.NET Framework 4.5부터 키워드(async
Visual Basic)로 특성 Async
이 지정된 모든 메서드는 비동기 메서드로 간주되며 C# 및 Visual Basic 컴파일러는 TAP을 사용하여 메서드를 비동기적으로 구현하는 데 필요한 변환을 수행합니다. 비동기 메서드는 System.Threading.Tasks.Task 또는 System.Threading.Tasks.Task<TResult> 개체를 반환해야 합니다. 후자의 경우 함수 본문은 a를 TResult
반환하고 컴파일러는 결과 작업 개체를 통해 이 결과를 사용할 수 있도록 합니다. 마찬가지로 메서드 본문 안에서 처리되지 않은 예외는 출력 태스크에 전달되어 결과 작업이 TaskStatus.Faulted 상태로 종료됩니다. 이 규칙의 예외는 OperationCanceledException (또는 파생된 형식)이 처리되지 않은 경우이며, 이 경우 결과 작업이 상태로 끝납니다 TaskStatus.Canceled .
수동으로 TAP 메서드 생성
구현을 보다 효율적으로 제어하기 위해 TAP 패턴을 수동으로 구현할 수 있습니다. 컴파일러는 System.Threading.Tasks 네임스페이스에서 제공되는 공용 인터페이스 영역 및 System.Runtime.CompilerServices 네임스페이스의 지원 형식에 의존합니다. TAP를 직접 구현하려면 TaskCompletionSource<TResult> 개체를 만들고, 비동기 작업을 수행한 다음 완료되면 SetResult, SetException, 또는 SetCanceled 메서드를 호출하거나 이러한 메서드 중 하나의 Try
버전을 호출합니다. TAP 메서드를 수동으로 구현하는 경우 표시된 비동기 작업이 완료되면 결과 작업을 완료해야 합니다. 다음은 그 예입니다.
public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object state)
{
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, ar =>
{
try { tcs.SetResult(stream.EndRead(ar)); }
catch (Exception exc) { tcs.SetException(exc); }
}, state);
return tcs.Task;
}
<Extension()>
Public Function ReadTask(stream As Stream, buffer() As Byte,
offset As Integer, count As Integer,
state As Object) As Task(Of Integer)
Dim tcs As New TaskCompletionSource(Of Integer)()
stream.BeginRead(buffer, offset, count, Sub(ar)
Try
tcs.SetResult(stream.EndRead(ar))
Catch exc As Exception
tcs.SetException(exc)
End Try
End Sub, state)
Return tcs.Task
End Function
하이브리드 접근 방식
TAP 패턴을 수동으로 구현하지만 구현의 핵심 논리를 컴파일러에 위임하는 것이 유용할 수 있습니다. 예를 들어 예외가 개체를 통해 System.Threading.Tasks.Task 노출되지 않고 메서드의 직접 호출자로 이스케이프될 수 있도록 컴파일러에서 생성된 비동기 메서드 외부에서 인수를 확인하려는 경우 하이브리드 방법을 사용할 수 있습니다.
public Task<int> MethodAsync(string input)
{
if (input == null) throw new ArgumentNullException("input");
return MethodAsyncInternal(input);
}
private async Task<int> MethodAsyncInternal(string input)
{
// code that uses await goes here
return value;
}
Public Function MethodAsync(input As String) As Task(Of Integer)
If input Is Nothing Then Throw New ArgumentNullException("input")
Return MethodAsyncInternal(input)
End Function
Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)
' code that uses await goes here
return value
End Function
이러한 위임이 유용한 또 다른 경우는 빠른 경로 최적화를 구현하고 캐시된 작업을 반환하려는 경우입니다.
작업 부하
컴퓨팅 바인딩된 작업과 I/O 바인딩된 비동기 작업을 모두 TAP 메서드로 구현할 수 있습니다. 그러나 TAP 메서드가 라이브러리에서 공개적으로 노출되는 경우 I/O 바인딩된 작업을 포함하는 워크로드에 대해서만 제공해야 합니다(계산을 포함할 수도 있지만 순수하게 계산해서는 안 됨). 메서드가 순수하게 컴퓨팅 바인딩된 경우 동기 구현으로만 노출되어야 합니다. 그런 다음 이 메서드를 사용하는 코드는 해당 동기 메서드의 호출을 작업으로 래핑하여 작업을 다른 스레드로 오프로드하거나 병렬 처리를 수행할지 여부를 선택할 수 있습니다. 또한 메서드가 I/O 바인딩된 경우 비동기 구현으로만 노출되어야 합니다.
컴퓨팅 바인딩된 작업
이 System.Threading.Tasks.Task 클래스는 계산 집약적인 작업을 나타내는 데 이상적입니다. 기본적으로 클래스 내에서 ThreadPool 특수 지원을 활용하여 효율적인 실행을 제공하고 비동기 계산이 실행되는 시기, 위치 및 방법을 크게 제어할 수 있습니다.
다음과 같은 방법으로 컴퓨팅 바인딩된 작업을 생성할 수 있습니다.
.NET Framework 4.5 이상 버전(.NET Core 및 .NET 5 이상 포함)에서 정적 Task.Run 메서드를 바로 가기 TaskFactory.StartNew로 사용합니다. 스레드 풀을 대상으로 하는 컴퓨팅 바인딩된 작업을 쉽게 시작하는 데 사용할 Run 수 있습니다. 컴퓨팅 바인딩된 작업을 시작하기 위한 기본 메커니즘입니다. 작업을 보다 세밀하게 제어하려는 경우에만 직접 사용합니다
StartNew
..NET Framework 4에서는 TaskFactory.StartNew 메서드를 사용하여 비동기적으로 실행할 대리자(일반적으로 Action<T> 또는 Func<TResult>)를 허용합니다. 대리자를 Action<T> 제공하는 경우 메서드는 해당 대리자의 비동기 실행을 나타내는 개체를 반환 System.Threading.Tasks.Task 합니다. Func<TResult> 대리자를 제공하면 메서드가 System.Threading.Tasks.Task<TResult> 객체를 반환합니다. 메서드의 StartNew 오버로드는 취소 토큰(CancellationToken), 작업 생성 옵션(TaskCreationOptions) 및 작업 스케줄러()TaskScheduler를 허용하며, 이 모든 작업은 작업의 예약 및 실행에 대한 세분화된 제어를 제공합니다. 현재 작업 스케줄러를 대상으로 하는 팩터리 인스턴스는 클래스의 Factory 정적 속성(Task)으로 사용할 수 있습니다. 예를 들면 다음과
Task.Factory.StartNew(…)
같습니다.작업을 별도로 생성하고 예약하려면 형식의
Task
생성자와Start
메서드를 사용합니다. 공용 메서드는 이미 시작된 작업만 반환해야 합니다.메서드의 오버로드를 사용하여 Task.ContinueWith를 호출합니다. 이 메서드는 다른 작업이 완료될 때 예약된 새 작업을 만듭니다. ContinueWith 일부 오버로드는 연속 작업의 예약 및 실행을 보다 효율적으로 제어하기 위해 취소 토큰, 연속 옵션 및 작업 스케줄러를 허용합니다.
TaskFactory.ContinueWhenAll 및 TaskFactory.ContinueWhenAny 메서드를 사용합니다. 이러한 메서드는 제공된 작업 집합의 전체 또는 일부가 완료될 때 예약된 새 작업을 만듭니다. 또한 이러한 메서드는 이러한 작업의 예약 및 실행을 제어하는 오버로드를 제공합니다.
컴퓨팅 바인딩된 작업에서 시스템은 작업 실행을 시작하기 전에 취소 요청을 수신하는 경우 예약된 작업의 실행을 방지할 수 있습니다. 따라서 취소 토큰(CancellationToken 개체)을 제공하는 경우 해당 토큰을 모니터링하는 비동기 코드에 전달할 수 있습니다. 또한 런타임에서 토큰을 모니터링할 수 있도록 StartNew
이전에 언급한 메서드 Run
Task
중 하나에 토큰을 제공할 수도 있습니다.
예를 들어 이미지를 렌더링하는 비동기 메서드를 고려해 보세요. 작업의 본문은 렌더링 중에 취소 요청이 도착하는 경우 코드가 일찍 종료되도록 취소 토큰을 폴링할 수 있습니다. 또한 렌더링이 시작되기 전에 취소 요청이 도착하는 경우 렌더링 작업을 방지해야 합니다.
internal Task<Bitmap> RenderAsync(
ImageData data, CancellationToken cancellationToken)
{
return Task.Run(() =>
{
var bmp = new Bitmap(data.Width, data.Height);
for(int y=0; y<data.Height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
for(int x=0; x<data.Width; x++)
{
// render pixel [x,y] into bmp
}
}
return bmp;
}, cancellationToken);
}
Friend Function RenderAsync(data As ImageData, cancellationToken As _
CancellationToken) As Task(Of Bitmap)
Return Task.Run(Function()
Dim bmp As New Bitmap(data.Width, data.Height)
For y As Integer = 0 to data.Height - 1
cancellationToken.ThrowIfCancellationRequested()
For x As Integer = 0 To data.Width - 1
' render pixel [x,y] into bmp
Next
Next
Return bmp
End Function, cancellationToken)
End Function
다음 조건 중 하나 이상이 참이면 계산 바운드 작업은 Canceled 상태로 끝납니다.
작업이 CancellationToken 상태로 전환되기 전에, 취소 요청은 생성 메서드(예:
StartNew
또는Run
)에 대한 인수로 제공된 Running 객체를 통해 전달됩니다.해당 작업의 본문 내에서 예외가 처리되지 않은 채로 남아 있으며, 이 예외는 작업에 전달된 것과 동일한 OperationCanceledException를 포함하고 있어, 해당 토큰이 취소 요청을 나타냅니다.
태스크 본문 내에서 다른 예외가 처리되지 않으면, 태스크는 Faulted 상태로 종료되며, 태스크를 기다리거나 결과에 액세스하려고 하면 예외가 발생합니다.
I/O 바인딩된 작업
전체 실행에 대해 스레드에서 직접 지원해서는 안 되는 작업을 만들려면 형식을 TaskCompletionSource<TResult> 사용합니다. 이 형식은 연결된 Task 인스턴스를 Task<TResult> 반환하는 속성을 노출합니다. 이 작업의 수명 주기는 TaskCompletionSource<TResult> 메서드, 예를 들어 SetResult, SetException, SetCanceled 및 해당 TrySet
변형에 의해 제어됩니다.
지정된 기간 후에 완료될 작업을 만들려고 합니다. 예를 들어 사용자 인터페이스에서 활동을 지연할 수 있습니다. 클래스 System.Threading.Timer은(는) 이미 지정된 기간 후에 대리자를 비동기적으로 호출할 수 있는 기능을 제공하며, TaskCompletionSource<TResult>을(를) 사용하여 타이머에 Task<TResult> 프론트를 추가할 수 있습니다. 예를 들면 다음과 같습니다.
public static Task<DateTimeOffset> Delay(int millisecondsTimeout)
{
TaskCompletionSource<DateTimeOffset> tcs = null;
Timer timer = null;
timer = new Timer(delegate
{
timer.Dispose();
tcs.TrySetResult(DateTimeOffset.UtcNow);
}, null, Timeout.Infinite, Timeout.Infinite);
tcs = new TaskCompletionSource<DateTimeOffset>(timer);
timer.Change(millisecondsTimeout, Timeout.Infinite);
return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of DateTimeOffset)
Dim tcs As TaskCompletionSource(Of DateTimeOffset) = Nothing
Dim timer As Timer = Nothing
timer = New Timer(Sub(obj)
timer.Dispose()
tcs.TrySetResult(DateTimeOffset.UtcNow)
End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)
tcs = New TaskCompletionSource(Of DateTimeOffset)(timer)
timer.Change(millisecondsTimeout, Timeout.Infinite)
Return tcs.Task
End Function
이 Task.Delay 메서드는 이러한 용도로 제공되며, 예를 들어 다른 비동기 메서드 내에서 이 메서드를 사용하여 비동기 폴링 루프를 구현할 수 있습니다.
public static async Task Poll(Uri url, CancellationToken cancellationToken,
IProgress<bool> progress)
{
while(true)
{
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
bool success = false;
try
{
await DownloadStringAsync(url);
success = true;
}
catch { /* ignore errors */ }
progress.Report(success);
}
}
Public Async Function Poll(url As Uri, cancellationToken As CancellationToken,
progress As IProgress(Of Boolean)) As Task
Do While True
Await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken)
Dim success As Boolean = False
Try
await DownloadStringAsync(url)
success = true
Catch
' ignore errors
End Try
progress.Report(success)
Loop
End Function
클래스에 TaskCompletionSource<TResult> 제네릭이 아닌 대응 항목이 없습니다. 그러나 Task<TResult>는 Task에서 파생되기 때문에 단순히 작업을 반환하는 I/O를 바인딩하는 메서드에 제네릭 TaskCompletionSource<TResult> 객체를 사용할 수 있습니다. 이렇게 하려면 더미 TResult
가 있는 소스를 사용할 수 있습니다. Boolean은 좋은 기본 선택이지만, Task를 Task<TResult>로 다운캐스트하는 것에 대해 걱정된다면, 대신 프라이빗 TResult
유형을 사용할 수 있습니다. 예를 들어 이전 예제의 메서드는 Delay
결과 오프셋(Task<DateTimeOffset>
)과 함께 현재 시간을 반환합니다. 이러한 결과 값이 필요하지 않은 경우 메서드를 다음과 같이 코딩할 수 있습니다(반환 형식의 변경 및 인수 TrySetResult변경에 유의하세요.)
public static Task<bool> Delay(int millisecondsTimeout)
{
TaskCompletionSource<bool> tcs = null;
Timer timer = null;
timer = new Timer(delegate
{
timer.Dispose();
tcs.TrySetResult(true);
}, null, Timeout.Infinite, Timeout.Infinite);
tcs = new TaskCompletionSource<bool>(timer);
timer.Change(millisecondsTimeout, Timeout.Infinite);
return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of Boolean)
Dim tcs As TaskCompletionSource(Of Boolean) = Nothing
Dim timer As Timer = Nothing
Timer = new Timer(Sub(obj)
timer.Dispose()
tcs.TrySetResult(True)
End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)
tcs = New TaskCompletionSource(Of Boolean)(timer)
timer.Change(millisecondsTimeout, Timeout.Infinite)
Return tcs.Task
End Function
혼합된 연산 집약적 및 I/O 집약적 작업
비동기 메서드는 컴퓨팅 바인딩 또는 I/O 바인딩된 작업으로만 제한되지 않지만 두 작업의 혼합을 나타낼 수 있습니다. 실제로 여러 비동기 작업은 종종 더 큰 혼합 작업으로 결합됩니다. 예를 들어 이전 예제의 메서드는 RenderAsync
계산 집약적인 작업을 수행하여 일부 입력 imageData
을 기반으로 이미지를 렌더링했습니다. 이 imageData
는 비동기적으로 액세스하는 웹 서비스에서 올 수 있습니다.
public async Task<Bitmap> DownloadDataAndRenderImageAsync(
CancellationToken cancellationToken)
{
var imageData = await DownloadImageDataAsync(cancellationToken);
return await RenderAsync(imageData, cancellationToken);
}
Public Async Function DownloadDataAndRenderImageAsync(
cancellationToken As CancellationToken) As Task(Of Bitmap)
Dim imageData As ImageData = Await DownloadImageDataAsync(cancellationToken)
Return Await RenderAsync(imageData, cancellationToken)
End Function
또한 이 예제에서는 여러 비동기 작업을 통해 단일 취소 토큰을 스레드할 수 있는 방법을 보여 줍니다. 자세한 내용은 작업 기반 비동기 패턴 사용의 취소 사용 섹션을 참조하세요.
참고하십시오
.NET