TAP(작업 기반 비동기 패턴)를 사용하여 비동기 작업을 사용하는 경우 콜백을 사용하여 차단 없이 대기할 수 있습니다. 작업의 경우 이 패턴은 다음과 같은 메서드를 Task.ContinueWith사용합니다. 언어 기반 비동기 지원은 일반 제어 흐름 내에서 비동기 작업을 대기할 수 있도록 하여 콜백을 숨기고 컴파일러 생성 코드는 이와 동일한 API 수준 지원을 제공합니다.
Await를 사용하여 실행 일시 중단
C#의 await 키워드와 Visual Basic의 Await 연산자를 사용하여 Task 및 Task<TResult> 개체에서 비동기적으로 대기할 수 있습니다.
Task를 기다릴 때, 식의 형식은 await에 속하며 void입니다. 기다리는 Task<TResult>의 경우, await 표현식은 TResult 유형입니다. 표현식은 await 비동기 메서드의 본문 내에서 사용되어야 합니다. (이러한 언어 기능은 .NET Framework 4.5에서 도입되었습니다.)
기본적으로, await 기능은 연속을 사용하여 작업에 콜백을 설치합니다. 이 콜백은 일시 중단 지점에서 비동기 메서드를 다시 시작합니다. 비동기 메서드가 다시 시작되면, 대기 중인 작업이 성공적으로 완료되고, 그것이 a Task<TResult>인 경우 해당 TResult이 반환됩니다.
Task 또는 Task<TResult>이 Canceled 상태로 끝났을 경우, OperationCanceledException 예외가 발생합니다.
Task 또는 Task<TResult>이(가) Faulted 상태로 종료되면, 이에 의해 오류가 발생한 예외가 던져집니다.
Task는 여러 예외로 인해 오류가 발생할 수 있지만 이들 예외 중 하나만 전파됩니다. 그러나 Task.Exception 속성은 모든 오류를 포함하는 AggregateException 예외를 반환합니다.
동기화 컨텍스트(SynchronizationContext개체)가 일시 중단 시 비동기 메서드를 실행하던 스레드와 연결된 경우(예: 속성이 아닌 SynchronizationContext.Current경우null) 컨텍스트의 Post 메서드를 사용하여 동일한 동기화 컨텍스트에서 비동기 메서드가 다시 시작됩니다. 그렇지 않으면 일시 중단 시 현재 작업 스케줄러(TaskScheduler 개체)를 사용합니다. 일반적으로 스레드 풀을 대상으로 하는 기본 작업 스케줄러(TaskScheduler.Default)입니다. 이 작업 스케줄러는 대기 중인 비동기 작업이 완료된 위치에서 다시 시작해야 하는지 또는 재개를 예약해야 하는지 여부를 결정합니다. 기본 스케줄러는 일반적으로 대기 중인 작업이 완료된 스레드에서 연속 작업을 실행할 수 있도록 허용합니다.
비동기 메서드를 호출하면 아직 완료되지 않은 대기 가능한 인스턴스에서 첫 번째 await 식이 호출자에게 반환될 때까지 함수 본문을 동기적으로 실행합니다. 비동기 메서드가 void를 반환하지 않으면, 진행 중인 계산을 나타내는 Task 또는 Task<TResult> 개체를 반환합니다. void가 아닌 비동기 메서드에서 return 문이 발견되거나 메서드 본문의 끝에 도달하면 작업이 최종 상태로 완료됩니다 RanToCompletion . 처리되지 않은 예외로 인해 컨트롤이 비동기 메서드의 본문을 벗어나면, 작업이 Faulted 상태로 끝납니다. 해당 예외가 OperationCanceledException인 경우, 태스크는 Canceled 상태로 끝납니다. 이러한 방식으로 결과 또는 예외는 결국 게시됩니다.
이 동작의 몇 가지 중요한 변형이 있습니다. 성능상의 이유로 작업이 대기할 때까지 작업이 이미 완료된 경우 컨트롤이 생성되지 않고 함수가 계속 실행됩니다. 또한 원래 컨텍스트로 돌아가는 것은 항상 원하는 동작이 아니며 변경할 수 있습니다. 이 동작은 다음 섹션에서 자세히 설명합니다.
Yield 및 ConfigureAwait을 사용하여 일시 중단 및 다시 시작 구성
여러 메서드는 비동기 메서드의 실행을 더 자세히 제어합니다. 예를 들어 이 메서드를 Task.Yield 사용하여 비동기 메서드에 수율 지점을 도입할 수 있습니다.
public class Task : …
{
public static YieldAwaitable Yield();
…
}
이 메서드는 비동기적으로 현재 컨텍스트에 다시 게시하거나 예약하는 것과 동일합니다.
public static async Task YieldLoopExample()
{
await Task.Run(async delegate
{
for (int i = 0; i < 1000000; i++)
{
await Task.Yield(); // fork the continuation into a separate work item
}
});
}
Public Async Function YieldLoopExample() As Task
Await Task.Run(Async Function()
For i As Integer = 0 To 999999
Await Task.Yield() ' fork the continuation into a separate work item
Next
End Function)
End Function
또한 비동기 메서드에서 Task.ConfigureAwait 일시 중단 및 재개를 보다 효율적으로 제어하기 위해 이 메서드를 사용할 수 있습니다. 앞에서 설명한 것처럼 기본적으로 현재 컨텍스트는 비동기 메서드가 일시 중단될 때 캡처되며 캡처된 컨텍스트는 다시 시작 시 비동기 메서드의 연속 작업을 호출하는 데 사용됩니다. 대부분의 경우 이것이 원하는 정확한 동작입니다. 다른 경우에는 연속 컨텍스트에 대해 신경 쓰지 않을 수 있으며 이러한 게시물을 원래 컨텍스트로 다시 사용하지 않도록 하여 더 나은 성능을 얻을 수 있습니다. 이 동작을 활성화하려면, Task.ConfigureAwait 메서드를 사용하여 대기 작업이 현재 컨텍스트에서 캡처 및 재개되지 않고, 대기 중인 비동기 작업이 완료된 위치에서 실행을 계속하도록 설정합니다.
await someTask.ConfigureAwait(continueOnCapturedContext:false);
Awaitables, ConfigureAwait 및 SynchronizationContext
await은 .뿐만 아니라 Task 충족하는 모든 형식에서 작동합니다. 호환 가능한 GetAwaiter 메서드를 제공하여, 반환된 형식에 IsCompleted, OnCompleted, GetResult 멤버가 있을 경우 해당 형식은 대기 가능하다고 합니다. 대부분의 공용 API에서 반환 Task, Task<TResult>또는 ValueTaskValueTask<TResult>. 특수 시나리오에 대해서만 사용자 지정 대기 가능을 사용합니다.
연속 작업에서 호출자의 컨텍스트가 필요하지 않은 경우에 사용합니다 ConfigureAwait . UI를 업데이트하는 앱 코드에서는 컨텍스트 캡처가 필요한 경우가 많습니다. 재사용 가능한 라이브러리 코드 ConfigureAwait(false) 에서는 일반적으로 불필요한 컨텍스트 홉을 방지하고 차단되는 호출자의 교착 상태 위험을 줄이기 때문에 선호됩니다.
ConfigureAwait(false) 연속 일정 예약은 변경하지만, ExecutionContext 흐름은 아닙니다. 컨텍스트 동작에 대한 자세한 설명은 ExecutionContext 및 SynchronizationContext를 참조하세요.
비동기 작업 취소
.NET Framework 4부터 취소를 지원하는 TAP 메서드는 취소 토큰(CancellationToken 개체)을 허용하는 오버로드를 하나 이상 제공합니다.
취소 토큰 원본(CancellationTokenSource 개체)을 통해 취소 토큰을 만듭니다. 소스의 Token 속성은 소스의 Cancel 메서드가 호출될 때 신호를 보내는 취소 토큰을 반환합니다.
var cts = new CancellationTokenSource();
string result = await DownloadStringTaskAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();
예를 들어 단일 웹 페이지를 다운로드하려는 경우 작업을 취소하고 개체를 만들고 CancellationTokenSource 해당 토큰을 TAP 메서드에 전달한 다음 작업을 취소할 준비가 되면 소스 메서드 Cancel 를 호출합니다.
var cts = new CancellationTokenSource();
IList<string> results = await Task.WhenAll(from url in urls select DownloadStringTaskAsync(url, cts.Token));
// at some point later, potentially on another thread
…
cts.Cancel();
또는 동일한 토큰을 작업의 선택적 하위 집합에 전달할 수 있습니다.
var cts = new CancellationTokenSource();
byte [] data = await DownloadDataAsync(url, cts.Token);
await SaveToDiskAsync(outputPath, data, CancellationToken.None);
… // at some point later, potentially on another thread
cts.Cancel();
중요합니다
모든 스레드는 취소 요청을 시작할 수 있습니다.
취소 토큰을 CancellationToken.None 수락하는 모든 메서드에 값을 전달하여 취소가 요청되지 않았음을 나타낼 수 있습니다. 이 값을 사용하면 속성이 CancellationToken.CanBeCanceled 반환 false되고 호출된 메서드가 그에 따라 최적화할 수 있습니다. 테스트를 위해 부울 값을 허용하는 생성자를 사용하여 인스턴스화된 미리 취소된 취소 토큰을 전달하여 토큰이 이미 취소되었거나 취소할 수 없는 상태로 시작해야 하는지 여부를 나타낼 수도 있습니다.
취소에 대한 이 방법은 다음과 같은 몇 가지 장점이 있습니다.
동일한 취소 토큰을 원하는 수의 비동기 및 동기 작업에 전달할 수 있습니다.
동일한 취소 요청은 여러 리스너에게 보낼 수 있습니다.
비동기 API의 개발자는 취소를 요청할 수 있는지 여부와 적용 시기를 완전히 제어할 수 있습니다.
API를 사용하는 코드는 취소 요청이 이동하는 비동기 호출을 선택적으로 결정할 수 있습니다.
진행률 모니터링
일부 비동기 메서드는 비동기 메서드에 전달하는 진행률 인터페이스를 통해 진행률을 표시합니다. 예를 들어 텍스트 문자열을 비동기적으로 다운로드하고 그 과정에서 지금까지 완료된 다운로드의 백분율을 포함하는 진행률 업데이트를 발생시키는 함수를 고려해 보세요. 다음과 같이 Windows Presentation Foundation(WPF) 애플리케이션에서 이러한 메서드를 사용할 수 있습니다.
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.IsEnabled = false;
try
{
txtResult.Text = await DownloadStringTaskAsync(txtUrl.Text,
new Progress<int>(p => pbDownloadProgress.Value = p));
}
finally { btnDownload.IsEnabled = true; }
}
기본 제공 작업 기반 조합기 사용
네임스페이스에는 System.Threading.Tasks 작업을 구성하고 작업하기 위한 몇 가지 방법이 포함되어 있습니다.
비고
이 섹션의 여러 코드 샘플은 Bitmap 사용합니다. System.Drawing.Common 패키지가 필요하며 Windows만 지원됩니다. 보여 주는 비동기 패턴은 모든 플랫폼에 적용됩니다. 플랫폼 간 이미징 라이브러리를 Windows 아닌 대상으로 대체합니다.
Task.Run (작업 실행)
클래스에는 작업을 Task 또는 Task<TResult> 형태로 스레드 풀에 쉽게 오프로드할 수 있는 몇 가지 Run 메서드가 포함되어 있습니다. 다음은 그 예입니다.
public static async Task TaskRunBasicExample()
{
int answer = 42;
string result = await Task.Run(() =>
{
// … do compute-bound work here
return answer.ToString();
});
Console.WriteLine(result);
}
Public Async Function TaskRunBasicExample() As Task
Dim answer As Integer = 42
Dim result As String = Await Task.Run(Function()
' … do compute-bound work here
Return answer.ToString()
End Function)
Console.WriteLine(result)
End Function
이러한 Run 메서드 중 일부는 Task.Run(Func<Task>) 오버로드처럼 TaskFactory.StartNew 메서드의 단축형으로 존재합니다. 이 오버로드를 사용하면 오프로드된 작업 내에서 사용할 await 수 있습니다. 다음은 그 예입니다.
public static async Task TaskRunAsyncExample()
{
Bitmap image = await Task.Run(async () =>
{
using Bitmap bmp1 = await Stubs.DownloadFirstImageAsync();
using Bitmap bmp2 = await Stubs.DownloadSecondImageAsync();
return Stubs.Mashup(bmp1, bmp2);
});
}
Public Async Function TaskRunAsyncExample() As Task
Dim image As Bitmap = Await Task.Run(Async Function()
Using bmp1 As Bitmap = Await Stubs.DownloadFirstImageAsync()
Using bmp2 As Bitmap = Await Stubs.DownloadSecondImageAsync()
Return Stubs.Mashup(bmp1, bmp2)
End Using
End Using
End Function)
End Function
이러한 오버로드는 작업 병렬 라이브러리에서 TaskFactory.StartNew 메서드를 Unwrap 확장 메서드와 함께 사용하는 것과 논리적으로 동일합니다.
Task.FromResult
FromResult 메서드는 데이터가 이미 사용 가능한 시나리오에서 사용하고, 이를 Task<TResult>로 리프팅된 작업 반환 메서드에서 반환하기만 하면 됩니다.
public static Task<int> GetValueAsync(string key)
{
int cachedValue;
return Stubs.TryGetCachedValue(out cachedValue) ?
Task.FromResult(cachedValue) :
GetValueAsyncInternal(key);
}
static async Task<int> GetValueAsyncInternal(string key)
{
await Task.Delay(1);
return 0;
}
Public Function GetValueAsync(key As String) As Task(Of Integer)
Dim cachedValue As Integer
If Stubs.TryGetCachedValue(cachedValue) Then
Return Task.FromResult(cachedValue)
Else
Return GetValueAsyncInternal(key)
End If
End Function
Private Async Function GetValueAsyncInternal(key As String) As Task(Of Integer)
Await Task.Delay(1)
Return 0
End Function
Task.WhenAll
WhenAll 메서드를 사용하여 작업으로 표시되는 여러 비동기 작업을 비동기적으로 대기합니다. 메서드에는 제네릭이 아닌 작업 집합 또는 균일하지 않은 제네릭 작업 집합(예: 여러 void 반환 작업을 비동기적으로 대기하거나 각 값이 다른 형식일 수 있는 여러 값 반환 메서드를 비동기적으로 대기)을 지원하고 균일한 제네릭 작업 집합(예: 여러 반환 메서드를 비동기적으로 대기)을 지원하는 오버로드가 여러 TResult개 있습니다.
여러 고객에게 전자 메일 메시지를 보내려고 하는 경우를 가정해 보겠습니다. 메시지를 보내기 전에 한 메시지가 완료되기를 기다리지 않도록 메시지를 겹칠 수 있습니다. 송신 작업이 완료된 시기와 오류가 발생하는지 여부도 확인할 수 있습니다.
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);
이 코드는 발생할 수 있는 예외를 명시적으로 처리하지 않지만, WhenAll에서 생성된 결과 작업에서 await의 예외가 전파될 수 있도록 합니다. 예외를 처리하려면 다음과 같은 코드를 사용합니다.
public static async Task WhenAllWithCatch()
{
IEnumerable<Task> asyncOps = from addr in Stubs.addrs select Stubs.SendMailAsync(addr);
try
{
await Task.WhenAll(asyncOps);
}
catch (Exception exc)
{
Console.WriteLine(exc);
}
}
Public Async Function WhenAllWithCatch() As Task
Dim asyncOps As IEnumerable(Of Task) = From addr In Stubs.addrs Select Stubs.SendMailAsync(addr)
Try
Await Task.WhenAll(asyncOps)
Catch exc As Exception
Console.WriteLine(exc)
End Try
End Function
이 경우 비동기 작업이 실패하면, 모든 예외가 AggregateException 예외로 통합되어, WhenAll 메서드에서 반환된 Task에 저장됩니다. 그러나 이러한 예외 중 하나만 await 키워드에 의해 전파됩니다. 모든 예외를 검사하려는 경우 다음과 같이 이전 코드를 다시 작성할 수 있습니다.
public static async Task WhenAllExamineExceptions()
{
Task[] asyncOps = (from addr in Stubs.addrs select Stubs.SendMailAsync(addr)).ToArray();
try
{
await Task.WhenAll(asyncOps);
}
catch (Exception exc)
{
foreach (Task faulted in asyncOps.Where(t => t.IsFaulted))
{
Console.WriteLine($"Faulted: {faulted.Exception}");
}
}
}
Public Async Function WhenAllExamineExceptions() As Task
Dim asyncOps As Task() = (From addr In Stubs.addrs Select Stubs.SendMailAsync(addr)).ToArray()
Try
Await Task.WhenAll(asyncOps)
Catch exc As Exception
For Each faulted As Task In asyncOps.Where(Function(t) t.IsFaulted)
Console.WriteLine($"Faulted: {faulted.Exception}")
Next
End Try
End Function
웹에서 여러 파일을 비동기적으로 다운로드하는 예를 생각해 보세요. 이 경우 모든 비동기 작업에는 같은 유형의 결과 형식이 있으며 결과에 쉽게 액세스할 수 있습니다.
string [] pages = await Task.WhenAll(
from url in urls select DownloadStringTaskAsync(url));
이전 void 반환 시나리오에서 설명한 것과 동일한 예외 처리 기술을 사용할 수 있습니다.
public static async Task WhenAllDownloadPagesExceptions()
{
Task<string>[] asyncOps =
(from url in Stubs.urls select Stubs.DownloadStringTaskAsync(url)).ToArray();
try
{
string[] pages = await Task.WhenAll(asyncOps);
Console.WriteLine(pages.Length);
}
catch (Exception exc)
{
foreach (Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
{
Console.WriteLine($"Faulted: {faulted.Exception}");
}
}
}
Public Async Function WhenAllDownloadPagesExceptions() As Task
Dim asyncOps As Task(Of String)() =
(From url In Stubs.urls Select Stubs.DownloadStringTaskAsync(url)).ToArray()
Try
Dim pages As String() = Await Task.WhenAll(asyncOps)
Console.WriteLine(pages.Length)
Catch exc As Exception
For Each faulted As Task(Of String) In asyncOps.Where(Function(t) t.IsFaulted)
Console.WriteLine($"Faulted: {faulted.Exception}")
Next
End Try
End Function
Task.WhenAny
이 메서드를 WhenAny 사용하여 태스크로 표시되는 여러 비동기 작업 중 하나만 완료될 때까지 비동기적으로 기다립니다. 이 메서드는 네 가지 기본 사용 사례를 제공합니다.
중복성: 작업을 여러 번 수행하고 먼저 완료하는 작업을 선택합니다(예: 단일 결과를 반환하는 여러 주식 견적 웹 서비스에 문의하고 가장 빠른 결과를 완료하는 웹 서비스 선택).
인터리빙: 여러 작업을 시작하고 모든 작업이 완료되기를 기다리지만, 완료될 때마다 처리합니다.
제한: 다른 작업이 완료될 때 추가 작업을 시작할 수 있도록 허용합니다. 이 시나리오는 인터리빙 시나리오의 확장입니다.
조기 구제: 예를 들어 작업 t1로 표시되는 작업은 다른 작업 t2를 사용하여 WhenAny 작업에서 그룹화할 수 있으며, WhenAny 작업을 기다릴 수 있습니다. 작업 t2는 t1이 완료되기 전에 WhenAny 작업이 완료되도록 하는 시간 초과, 취소, 또는 다른 신호를 나타낼 수 있습니다.
중복성
주식을 구입할지 여부를 결정하려는 경우를 고려합니다. 신뢰할 수 있는 몇 가지 재고 추천 웹 서비스가 있지만 일일 부하에 따라 각 서비스가 서로 다른 시간에 느려질 수 있습니다. WhenAny 작업이 완료되면 이 메서드를 사용하여 알림을 받습니다.
public static async Task WhenAnyRedundancy(string symbol)
{
var recommendations = new List<Task<bool>>()
{
Stubs.GetBuyRecommendation1Async(symbol),
Stubs.GetBuyRecommendation2Async(symbol),
Stubs.GetBuyRecommendation3Async(symbol)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
if (await recommendation) Stubs.BuyStock(symbol);
}
Public Async Function WhenAnyRedundancy(symbol As String) As Task
Dim recommendations As New List(Of Task(Of Boolean)) From {
Stubs.GetBuyRecommendation1Async(symbol),
Stubs.GetBuyRecommendation2Async(symbol),
Stubs.GetBuyRecommendation3Async(symbol)
}
Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(recommendations)
If Await recommendation Then Stubs.BuyStock(symbol)
End Function
성공적으로 완료된 모든 작업의 래핑되지 않은 결과를 반환하는 WhenAll와 달리, WhenAny는 완료된 작업을 반환합니다. 작업이 실패하는 경우 작업이 실패했음을 알아야 하며, 작업이 성공하면 반환 값이 연결된 작업을 알고 있는 것이 중요합니다. 따라서 반환된 작업의 결과에 액세스하거나 이 예제와 같이 추가로 대기해야 합니다.
마찬가지로 WhenAll예외를 수용할 수 있어야 합니다. 완료된 작업을 다시 받으면, 반환된 태스크에서 오류가 전파되기를 기다렸다가 적절하게 처리할 수 있습니다. 예를 들어 다음과 같습니다.
public static async Task WhenAnyRetryOnException(string symbol)
{
Task<bool>[] allRecommendations = new Task<bool>[]
{
Stubs.GetBuyRecommendation1Async(symbol),
Stubs.GetBuyRecommendation2Async(symbol),
Stubs.GetBuyRecommendation3Async(symbol)
};
var remaining = allRecommendations.ToList();
while (remaining.Count > 0)
{
Task<bool> recommendation = await Task.WhenAny(remaining);
try
{
if (await recommendation) Stubs.BuyStock(symbol);
break;
}
catch (WebException)
{
remaining.Remove(recommendation);
}
}
}
Public Async Function WhenAnyRetryOnException(symbol As String) As Task
Dim allRecommendations As Task(Of Boolean)() = {
Stubs.GetBuyRecommendation1Async(symbol),
Stubs.GetBuyRecommendation2Async(symbol),
Stubs.GetBuyRecommendation3Async(symbol)
}
Dim remaining As List(Of Task(Of Boolean)) = allRecommendations.ToList()
While remaining.Count > 0
Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(remaining)
Try
If Await recommendation Then Stubs.BuyStock(symbol)
Exit While
Catch ex As WebException
remaining.Remove(recommendation)
End Try
End While
End Function
또한 첫 번째 작업이 성공적으로 완료되더라도 후속 작업이 실패할 수 있습니다. 이 시점에서 예외를 처리하기 위한 몇 가지 옵션이 있습니다. 시작된 모든 작업이 완료될 때까지 기다릴 수 있으며, 이 경우 메서드를 WhenAll 사용하거나 모든 예외가 중요하고 기록되어야 한다고 결정할 수 있습니다. 이 시나리오에서는 작업을 비동기적으로 완료할 때 연속 작업을 사용하여 알림을 받을 수 있습니다.
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => { if (t.IsFaulted) Log(t.Exception); });
}
또는:
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}
또는 심지어
private static async void LogCompletionIfFailed(IEnumerable<Task> tasks)
{
foreach (var task in tasks)
{
try { await task; }
catch (Exception exc) { Stubs.Log(exc); }
}
}
Private Async Sub LogCompletionIfFailed(tasks As IEnumerable(Of Task))
For Each task In tasks
Try
Await task
Catch exc As Exception
Stubs.Log(exc)
End Try
Next
End Sub
마지막으로, 나머지 작업을 모두 취소할 수 있습니다.
public static async Task WhenAnyCancelRemainder(string symbol)
{
var cts = new CancellationTokenSource();
var recommendations = new List<Task<bool>>()
{
Stubs.GetBuyRecommendation1Async(symbol, cts.Token),
Stubs.GetBuyRecommendation2Async(symbol, cts.Token),
Stubs.GetBuyRecommendation3Async(symbol, cts.Token)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
cts.Cancel();
if (await recommendation) Stubs.BuyStock(symbol);
}
Public Async Function WhenAnyCancelRemainder(symbol As String) As Task
Dim cts As New CancellationTokenSource()
Dim recommendations As New List(Of Task(Of Boolean)) From {
Stubs.GetBuyRecommendation1Async(symbol, cts.Token),
Stubs.GetBuyRecommendation2Async(symbol, cts.Token),
Stubs.GetBuyRecommendation3Async(symbol, cts.Token)
}
Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(recommendations)
cts.Cancel()
If Await recommendation Then Stubs.BuyStock(symbol)
End Function
인터리빙
웹에서 이미지를 다운로드하고 각 이미지를 처리하는 경우를 고려합니다(예: UI 컨트롤에 이미지 추가). UI 스레드에서 이미지를 순차적으로 처리하지만 가능한 한 동시에 이미지를 다운로드하려고 합니다. 또한 이미지를 모두 다운로드할 때까지 기다리지 않고 UI에 추가하고 싶을 것입니다. 대신 완료할 때마다 추가하려고 합니다.
public static async Task WhenAnyInterleaving(string[] imageUrls)
{
List<Task<Bitmap>> imageTasks =
(from imageUrl in imageUrls select Stubs.GetBitmapAsync(imageUrl)).ToList();
while (imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
Console.WriteLine($"Got image: {image.Width}x{image.Height}");
}
catch { }
}
}
Public Async Function WhenAnyInterleaving(imageUrls As String()) As Task
Dim imageTasks As List(Of Task(Of Bitmap)) =
(From imageUrl In imageUrls Select Stubs.GetBitmapAsync(imageUrl)).ToList()
While imageTasks.Count > 0
Try
Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
imageTasks.Remove(imageTask)
Dim image As Bitmap = Await imageTask
Console.WriteLine($"Got image: {image.Width}x{image.Height}")
Catch
End Try
End While
End Function
또한 다운로드한 이미지에 대한 계산 집약적인 처리를 포함하는 시나리오에 ThreadPool 인터리빙을 적용할 수 있습니다. 예를 들면 다음과 같습니다.
public static async Task WhenAnyInterleavingWithProcessing(string[] imageUrls)
{
List<Task<Bitmap>> imageTasks =
(from imageUrl in imageUrls
select Stubs.GetBitmapAsync(imageUrl)
.ContinueWith(t => Stubs.ConvertImage(t.Result))).ToList();
while (imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
Console.WriteLine($"Got image: {image.Width}x{image.Height}");
}
catch { }
}
}
Public Async Function WhenAnyInterleavingWithProcessing(imageUrls As String()) As Task
Dim imageTasks As List(Of Task(Of Bitmap)) =
(From imageUrl In imageUrls
Select Stubs.GetBitmapAsync(imageUrl).ContinueWith(Function(t) Stubs.ConvertImage(t.Result))).ToList()
While imageTasks.Count > 0
Try
Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
imageTasks.Remove(imageTask)
Dim image As Bitmap = Await imageTask
Console.WriteLine($"Got image: {image.Width}x{image.Height}")
Catch
End Try
End While
End Function
스로틀링
인터리빙 예제를 생각해보되, 사용자가 너무 많은 이미지를 다운로드하고 있어 다운로드 속도를 제한해야 하는 상황을 고려하십시오. 예를 들어 특정 수의 다운로드만 동시에 수행하려고 합니다. 이 목표를 달성하려면 비동기 작업의 하위 집합을 시작합니다. 작업이 완료되면 추가 작업을 시작하여 해당 작업을 수행할 수 있습니다.
public static async Task WhenAnyThrottling(Uri[] uriList)
{
const int CONCURRENCY_LEVEL = 15;
int nextIndex = 0;
var imageTasks = new List<Task<Bitmap>>();
while (nextIndex < CONCURRENCY_LEVEL && nextIndex < uriList.Length)
{
imageTasks.Add(Stubs.GetBitmapAsync(uriList[nextIndex].ToString()));
nextIndex++;
}
while (imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
Console.WriteLine($"Got image: {image.Width}x{image.Height}");
}
catch (Exception exc) { Stubs.Log(exc); }
if (nextIndex < uriList.Length)
{
imageTasks.Add(Stubs.GetBitmapAsync(uriList[nextIndex].ToString()));
nextIndex++;
}
}
}
Public Async Function WhenAnyThrottling(uriList As Uri()) As Task
Const CONCURRENCY_LEVEL As Integer = 15
Dim nextIndex As Integer = 0
Dim imageTasks As New List(Of Task(Of Bitmap))
While nextIndex < CONCURRENCY_LEVEL AndAlso nextIndex < uriList.Length
imageTasks.Add(Stubs.GetBitmapAsync(uriList(nextIndex).ToString()))
nextIndex += 1
End While
While imageTasks.Count > 0
Try
Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
imageTasks.Remove(imageTask)
Dim image As Bitmap = Await imageTask
Console.WriteLine($"Got image: {image.Width}x{image.Height}")
Catch exc As Exception
Stubs.Log(exc)
End Try
If nextIndex < uriList.Length Then
imageTasks.Add(Stubs.GetBitmapAsync(uriList(nextIndex).ToString()))
nextIndex += 1
End If
End While
End Function
조기 구제 금융
사용자의 취소 요청에 동시에 응답하는 동안 작업이 완료되기를 비동기적으로 기다리는 것을 고려합니다(예: 사용자가 취소 단추를 클릭). 다음 코드에서는 이 시나리오를 보여 줍니다.
class EarlyBailoutUI
{
private CancellationTokenSource? m_cts;
public void btnCancel_Click(object sender, EventArgs e)
{
if (m_cts != null) m_cts.Cancel();
}
public async void btnRun_Click(object sender, EventArgs e)
{
m_cts = new CancellationTokenSource();
try
{
Task<Bitmap> imageDownload = Stubs.GetBitmapAsync("url");
await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token);
if (imageDownload.IsCompleted)
{
Bitmap image = await imageDownload;
Stubs.Log(image);
}
else imageDownload.ContinueWith(t => Stubs.Log(t));
}
finally { }
}
}
Class EarlyBailoutUI
Private m_cts As CancellationTokenSource
Public Sub btnCancel_Click(sender As Object, e As EventArgs)
If m_cts IsNot Nothing Then m_cts.Cancel()
End Sub
Public Async Sub btnRun_Click(sender As Object, e As EventArgs)
m_cts = New CancellationTokenSource()
Try
Dim imageDownload As Task(Of Bitmap) = Stubs.GetBitmapAsync("url")
Await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token)
If imageDownload.IsCompleted Then
Dim image As Bitmap = Await imageDownload
Stubs.Log(image)
Else
imageDownload.ContinueWith(Sub(t) Stubs.Log(t))
End If
Finally
End Try
End Sub
End Class
이 구현은 구제하기로 결정하는 즉시 사용자 인터페이스를 다시 사용하도록 설정하지만 기본 비동기 작업을 취소하지는 않습니다. 또 다른 대안은 취소 요청으로 인해 일찍 종료될 수 있으므로 작업이 완료될 때까지 사용자 인터페이스를 다시 설정하지 않고 보류 중인 작업을 취소하는 것입니다.
class EarlyBailoutWithTokenUI
{
private CancellationTokenSource? m_cts;
public async void btnRun_Click(object sender, EventArgs e)
{
m_cts = new CancellationTokenSource();
try
{
Task<Bitmap> imageDownload = Stubs.GetBitmapAsync("url", m_cts.Token);
await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token);
Bitmap image = await imageDownload;
Stubs.Log(image);
}
catch (OperationCanceledException) { }
finally { }
}
}
Class EarlyBailoutWithTokenUI
Private m_cts As CancellationTokenSource
Public Async Sub btnRun_Click(sender As Object, e As EventArgs)
m_cts = New CancellationTokenSource()
Try
Dim imageDownload As Task(Of Bitmap) = Stubs.GetBitmapAsync("url", m_cts.Token)
Await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token)
Dim image As Bitmap = Await imageDownload
Stubs.Log(image)
Catch ex As OperationCanceledException
Finally
End Try
End Sub
End Class
초기 구제의 또 다른 예는 다음 섹션에서 논의된 바와 같이 WhenAny 메서드와 Delay 메서드를 함께 사용하는 것입니다.
작업 지연
메서드를 Task.Delay 사용하여 비동기 메서드의 실행에 일시 중지를 추가합니다. 이 일시 중지는 폴링 루프를 빌드하고 미리 결정된 기간 동안 사용자 입력 처리를 지연하는 등 다양한 종류의 기능에 유용합니다. Task.Delay 메서드를 Task.WhenAny와 함께 사용하여 'await'에서 시간 초과를 구현할 수도 있습니다.
더 큰 비동기 작업의 일부인 작업(예: ASP.NET 웹 서비스)을 완료하는 데 너무 오래 걸리는 경우 전체 작업이 완료되지 않을 경우 특히 문제가 발생할 수 있습니다. 이러한 이유로 비동기 작업을 대기할 때 타임아웃을 설정할 수 있어야 합니다. 동기 Task.Wait및 Task.WaitAllTask.WaitAny 메서드는 제한 시간 값을 허용하지만 해당 TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny 메서드와 앞에서 언급한 메서드는 Task.WhenAll/Task.WhenAny 그렇지 않습니다. 대신, Task.Delay와 Task.WhenAny를 함께 사용하여 시간 초과를 구현합니다.
예를 들어 UI 애플리케이션에서 이미지를 다운로드하고 이미지를 다운로드하는 동안 UI를 사용하지 않도록 설정하려는 경우를 가정합니다. 그러나 다운로드 시간이 너무 오래 걸리는 경우 UI를 다시 사용하도록 설정하고 다운로드를 취소하려고 합니다.
public static async Task<Bitmap?> DownloadWithTimeout(string url)
{
Task<Bitmap> download = Stubs.GetBitmapAsync(url);
if (download == await Task.WhenAny(download, Task.Delay(3000)))
{
return await download;
}
else
{
var ignored = download.ContinueWith(
t => Trace($"Task finally completed: {t.Status}"));
return null;
}
}
static void Trace(string message) => Console.WriteLine(message);
Public Async Function DownloadWithTimeout(url As String) As Task(Of Bitmap)
Dim download As Task(Of Bitmap) = Stubs.GetBitmapAsync(url)
If download Is Await Task.WhenAny(download, Task.Delay(3000)) Then
Return Await download
Else
Dim ignored = download.ContinueWith(Sub(t) TraceMsg($"Task finally completed: {t.Status}"))
Return Nothing
End If
End Function
작업을 반환하기 때문에 WhenAll 동일한 원칙이 여러 다운로드에 적용됩니다.
public static async Task<Bitmap[]?> DownloadMultipleWithTimeout(string[] imageUrls)
{
Task<Bitmap[]> downloads =
Task.WhenAll(from url in imageUrls select Stubs.GetBitmapAsync(url));
if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
{
return await downloads;
}
else
{
downloads.ContinueWith(t => Stubs.Log(t));
return null;
}
}
Public Async Function DownloadMultipleWithTimeout(imageUrls As String()) As Task(Of Bitmap())
Dim downloads As Task(Of Bitmap()) =
Task.WhenAll(From url In imageUrls Select Stubs.GetBitmapAsync(url))
If downloads Is Await Task.WhenAny(downloads, Task.Delay(3000)) Then
Return Await downloads
Else
downloads.ContinueWith(Sub(t) Stubs.Log(t))
Return Nothing
End If
End Function
작업 기반 조합기 구축
태스크는 비동기 작업을 완전히 나타내고 작업과 조인하고 결과를 검색하는 동기 및 비동기 기능을 제공할 수 있으므로 작업을 구성하는 결합자의 유용한 라이브러리를 빌드하여 더 큰 패턴을 작성할 수 있습니다. 이전 섹션에서 설명한 것처럼 .NET에는 몇 가지 기본 제공 조합기가 포함되어 있지만 직접 빌드할 수도 있습니다. 다음 섹션에서는 잠재적인 결합자 메서드 및 형식의 몇 가지 예를 제공합니다.
오류 발생 시 재시도
대부분의 경우 이전 시도가 실패하는 경우 작업을 다시 시도하려고 합니다. 동기 코드의 경우 다음 예제와 같이 RetryOnFault 도우미 메서드를 빌드하여 이 작업을 수행할 수 있습니다.
public static T RetryOnFault<T>(Func<T> function, int maxTries)
{
for (int i = 0; i < maxTries; i++)
{
try { return function(); }
catch { if (i == maxTries - 1) throw; }
}
return default(T)!;
}
Public Function RetryOnFaultSync(Of T)(func As Func(Of T), maxTries As Integer) As T
For i As Integer = 0 To maxTries - 1
Try
Return func()
Catch
If i = maxTries - 1 Then Throw
End Try
Next
Return Nothing
End Function
TAP로 구현된 비동기 작업에 대해 거의 동일한 도우미 메서드를 빌드하여 작업을 반환할 수 있습니다.
public static async Task<T> RetryOnFault<T>(Func<Task<T>> function, int maxTries)
{
for (int i = 0; i < maxTries; i++)
{
try { return await function().ConfigureAwait(false); }
catch { if (i == maxTries - 1) throw; }
}
return default(T)!;
}
Public Async Function RetryOnFault(Of T)(func As Func(Of Task(Of T)), maxTries As Integer) As Task(Of T)
For i As Integer = 0 To maxTries - 1
Try
Return Await func().ConfigureAwait(False)
Catch
If i = maxTries - 1 Then Throw
End Try
Next
Return Nothing
End Function
그런 다음 이 결합자를 사용하여 애플리케이션의 논리로 재시도를 인코딩할 수 있습니다. 다음은 그 예입니다.
// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
() => DownloadStringTaskAsync(url), 3);
RetryOnFault 기능을 더 확장할 수 있습니다. 예를 들어 함수는 다시 시도 간에 호출하는 다른 Func<Task> 함수를 수락하여 작업을 다시 시도할 시기를 결정할 수 있습니다. 다음은 그 예입니다.
public static async Task<T> RetryOnFaultWithDelay<T>(
Func<Task<T>> function, int maxTries, Func<Task> retryWhen)
{
for (int i = 0; i < maxTries; i++)
{
try { return await function().ConfigureAwait(false); }
catch { if (i == maxTries - 1) throw; }
await retryWhen().ConfigureAwait(false);
}
return default(T)!;
}
Public Async Function RetryOnFaultWithDelay(Of T)(
func As Func(Of Task(Of T)), maxTries As Integer, retryWhen As Func(Of Task)) As Task(Of T)
For i As Integer = 0 To maxTries - 1
Try
Return Await func().ConfigureAwait(False)
Catch
If i = maxTries - 1 Then Throw
End Try
Await retryWhen().ConfigureAwait(False)
Next
Return Nothing
End Function
그런 다음 다음과 같이 함수를 사용하여 작업을 다시 시도하기 전에 잠시 기다릴 수 있습니다.
// Download the URL, trying up to three times in case of failure,
// and delaying for a second between retries
string pageContents = await RetryOnFault(
() => DownloadStringTaskAsync(url), 3, () => Task.Delay(1000));
NeedOnlyOne
경우에 따라 중복성을 활용하여 작업의 대기 시간과 성공 가능성을 향상시킬 수 있습니다. 주식 견적을 제공하는 여러 웹 서비스를 고려하지만, 하루 중 다양한 시간에 각 서비스는 서로 다른 수준의 품질 및 응답 시간을 제공할 수 있습니다. 이러한 변동을 처리하기 위해 모든 웹 서비스에 요청을 실행하고, 응답을 받는 즉시 나머지 요청을 취소할 수 있습니다. 도우미 함수를 구현하여 여러 작업을 시작하고 대기한 다음 나머지 작업을 취소하는 일반적인 패턴을 보다 쉽게 구현할 수 있습니다. 다음 예제의 함수는 이 NeedOnlyOne 시나리오를 보여 줍니다.
public static async Task<T> NeedOnlyOne<T>(
params Func<CancellationToken, Task<T>>[] functions)
{
var cts = new CancellationTokenSource();
var tasks = (from function in functions
select function(cts.Token)).ToArray();
var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
cts.Cancel();
foreach (var task in tasks)
{
var ignored = task.ContinueWith(
t => Stubs.Log(t), TaskContinuationOptions.OnlyOnFaulted);
}
return await completed;
}
Public Async Function NeedOnlyOne(Of T)(
ParamArray functions As Func(Of CancellationToken, Task(Of T))()) As Task(Of T)
Dim cts As New CancellationTokenSource()
Dim tasks As Task(Of T)() = (From func In functions Select func(cts.Token)).ToArray()
Dim completed As Task(Of T) = Await Task.WhenAny(tasks).ConfigureAwait(False)
cts.Cancel()
For Each task In tasks
Dim ignored = task.ContinueWith(
Sub(tsk) Stubs.Log(tsk), TaskContinuationOptions.OnlyOnFaulted)
Next
Return Await completed
End Function
그런 다음 다음과 같이 이 함수를 사용할 수 있습니다.
double currentPrice = await NeedOnlyOne(
ct => GetCurrentPriceFromServer1Async("msft", ct),
ct => GetCurrentPriceFromServer2Async("msft", ct),
ct => GetCurrentPriceFromServer3Async("msft", ct));
교차 처리 작업
WhenAny 이 메서드를 사용하여 인터리빙 시나리오를 지원하면 큰 작업 집합으로 작업할 때 성능 문제가 발생할 수 있습니다.
WhenAny 호출 시마다 각 작업에 연속 작업을 등록합니다. N개의 태스크에 대해 이 프로세스는 인터리빙 작업의 수명 동안 O(N2) 연속 작업을 만듭니다. 대규모 작업 집합으로 작업하는 경우 다음 예제에서 조합기를Interleaved 사용하여 성능 문제를 해결합니다.
public static IEnumerable<Task<T>> Interleaved<T>(IEnumerable<Task<T>> tasks)
{
var inputTasks = tasks.ToList();
var sources = (from _ in Enumerable.Range(0, inputTasks.Count)
select new TaskCompletionSource<T>()).ToList();
int nextTaskIndex = -1;
foreach (var inputTask in inputTasks)
{
inputTask.ContinueWith(completed =>
{
var source = sources[Interlocked.Increment(ref nextTaskIndex)];
if (completed.IsFaulted)
source.TrySetException(completed.Exception!.InnerExceptions);
else if (completed.IsCanceled)
source.TrySetCanceled();
else
source.TrySetResult(completed.Result);
}, CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
return from source in sources
select source.Task;
}
Public Function Interleaved(Of T)(tasks As IEnumerable(Of Task(Of T))) As IEnumerable(Of Task(Of T))
Dim inputTasks As List(Of Task(Of T)) = tasks.ToList()
Dim sources As List(Of TaskCompletionSource(Of T)) =
(From _i In Enumerable.Range(0, inputTasks.Count) Select New TaskCompletionSource(Of T)()).ToList()
Dim indexRef As Integer() = {-1}
For Each inputTask In inputTasks
inputTask.ContinueWith(Sub(completed)
Dim idx = Interlocked.Increment(indexRef(0))
Dim source = sources(idx)
If completed.IsFaulted Then
source.TrySetException(completed.Exception.InnerExceptions)
ElseIf completed.IsCanceled Then
source.TrySetCanceled()
Else
source.TrySetResult(completed.Result)
End If
End Sub,
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default)
Next
Return From source In sources Select source.Task
End Function
조합기를 사용하여 완료되는 작업의 결과를 처리합니다. 다음은 그 예입니다.
IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
int result = await task;
…
}
모두 또는 첫 번째 예외 시
특정 분산/수집 시나리오에서는 하나의 오류가 발생하지 않는 한 집합의 모든 작업을 기다릴 수 있습니다. 이 경우 예외가 발생하는 즉시 대기를 중지하려고 합니다. 다음 예제와 같이 WhenAllOrFirstException 결합자 메서드를 사용하여 해당 동작을 수행할 수 있습니다.
public static Task<T[]> WhenAllOrFirstException<T>(IEnumerable<Task<T>> tasks)
{
var inputs = tasks.ToList();
var ce = new CountdownEvent(inputs.Count);
var tcs = new TaskCompletionSource<T[]>();
Action<Task> onCompleted = (Task completed) =>
{
if (completed.IsFaulted)
tcs.TrySetException(completed.Exception!.InnerExceptions);
if (ce.Signal() && !tcs.Task.IsCompleted)
tcs.TrySetResult(inputs.Select(t => ((Task<T>)t).Result).ToArray());
};
foreach (var t in inputs) t.ContinueWith(onCompleted);
return tcs.Task;
}
Public Function WhenAllOrFirstException(Of T)(tasks As IEnumerable(Of Task(Of T))) As Task(Of T())
Dim inputs As List(Of Task(Of T)) = tasks.ToList()
Dim ce As New CountdownEvent(inputs.Count)
Dim tcs As New TaskCompletionSource(Of T())()
Dim onCompleted As Action(Of Task) = Sub(completed As Task)
If completed.IsFaulted Then
tcs.TrySetException(completed.Exception.InnerExceptions)
End If
If ce.Signal() AndAlso Not tcs.Task.IsCompleted Then
tcs.TrySetResult(inputs.Select(Function(taskItem) DirectCast(taskItem, Task(Of T)).Result).ToArray())
End If
End Sub
For Each t In inputs
t.ContinueWith(onCompleted)
Next
Return tcs.Task
End Function
작업 기반 데이터 구조 빌드
사용자 지정 작업 기반 결합자를 구축하는 기능 외에도, Task 및 Task<TResult>가 비동기 작업의 결과와 그 작업과의 동기화에 필요한 요소들을 모두 나타내면 이는 비동기 시나리오에서 사용할 사용자 지정 데이터 구조를 구축하기에 강력한 데이터 구조가 됩니다.
비동기 캐시
작업의 한 가지 중요한 측면은 여러 소비자에게 전달할 수 있다는 것입니다. 모든 소비자는 이를 기다리고, 연속 작업을 등록하고, 결과 또는 예외(있는 Task<TResult>경우) 등을 가져올 수 있습니다. 이러한 측면은 TaskTask<TResult> 비동기 캐싱 인프라에서 사용할 수 있도록 완벽하게 적합합니다. 다음은 Task<TResult> 위에 구축된 작지만 강력한 비동기 캐시의 예시입니다.
public class AsyncCache<TKey, TValue> where TKey : notnull
{
private readonly Func<TKey, Task<TValue>> _valueFactory;
private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;
public AsyncCache(Func<TKey, Task<TValue>> valueFactory)
{
if (valueFactory == null) throw new ArgumentNullException(nameof(valueFactory));
_valueFactory = valueFactory;
_map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
}
public Task<TValue> this[TKey key]
{
get
{
if (key == null) throw new ArgumentNullException(nameof(key));
return _map.GetOrAdd(key, toAdd =>
new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
}
}
}
Public Class AsyncCache(Of TKey, TValue)
Private ReadOnly _valueFactory As Func(Of TKey, Task(Of TValue))
Private ReadOnly _map As New ConcurrentDictionary(Of TKey, Lazy(Of Task(Of TValue)))()
Public Sub New(valueFactory As Func(Of TKey, Task(Of TValue)))
If valueFactory Is Nothing Then Throw New ArgumentNullException(NameOf(valueFactory))
_valueFactory = valueFactory
End Sub
Default Public ReadOnly Property Item(key As TKey) As Task(Of TValue)
Get
If key Is Nothing Then Throw New ArgumentNullException(NameOf(key))
Return _map.GetOrAdd(key, Function(toAdd) New Lazy(Of Task(Of TValue))(Function() _valueFactory(toAdd))).Value
End Get
End Property
End Class
AsyncCache<TKey,TValue> 클래스는 생성자에 대한 대리자로서 TKey를 입력으로 받고 Task<TResult>를 반환하는 함수를 수락합니다. 내부 사전은 캐시에서 이전에 액세스한 값을 저장하고 AsyncCache 캐시가 동시에 액세스되더라도 키당 하나의 작업만 생성하도록 합니다.
예를 들어 다운로드한 웹 페이지에 대한 캐시를 빌드할 수 있습니다.
private AsyncCache<string,string> m_webPages =
new AsyncCache<string,string>(DownloadStringTaskAsync);
그런 다음 웹 페이지의 콘텐츠가 필요할 때마다 비동기 메서드에서 이 캐시를 사용할 수 있습니다. 클래스는 AsyncCache 가능한 한 적은 수의 페이지를 다운로드하고 결과를 캐시합니다.
static AsyncCache<string, string> m_webPages =
new AsyncCache<string, string>(url => Stubs.DownloadStringTaskAsync(url));
public static async Task UseWebPageCache(string url)
{
string contents = await m_webPages[url];
Console.WriteLine(contents.Length);
}
Private m_webPages As New AsyncCache(Of String, String)(Function(url) Stubs.DownloadStringTaskAsync(url))
Public Async Function UseWebPageCache(url As String) As Task
Dim contents As String = Await m_webPages(url)
Console.WriteLine(contents.Length)
End Function
AsyncProducerConsumerCollection
작업을 사용하여 비동기 활동을 조정하기 위한 데이터 구조를 빌드할 수도 있습니다. 클래식 병렬 디자인 패턴 중 하나인 생산자/소비자를 고려합니다. 이 패턴에서 생산자는 소비자가 사용하는 데이터를 생성하고 생산자와 소비자는 병렬로 실행할 수 있습니다. 예를 들어 소비자는 항목 1을 처리합니다. 이 항목은 현재 항목 2를 생성하는 생산자가 이전에 생성했습니다. 생산자/소비자 패턴의 경우 소비자에게 새 데이터를 알리고 사용 가능한 경우 찾을 수 있도록 생산자가 만든 작업을 저장하기 위해 항상 일부 데이터 구조가 필요합니다.
다음은 비동기 메서드를 생산자 및 소비자로 사용할 수 있도록 하는 작업 위에 빌드된 간단한 데이터 구조입니다.
public class AsyncProducerConsumerCollection<T>
{
private readonly Queue<T> m_collection = new Queue<T>();
private readonly Queue<TaskCompletionSource<T>> m_waiting =
new Queue<TaskCompletionSource<T>>();
public void Add(T item)
{
TaskCompletionSource<T>? tcs = null;
lock (m_collection)
{
if (m_waiting.Count > 0) tcs = m_waiting.Dequeue();
else m_collection.Enqueue(item);
}
if (tcs != null) tcs.TrySetResult(item);
}
public Task<T> Take()
{
lock (m_collection)
{
if (m_collection.Count > 0)
{
return Task.FromResult(m_collection.Dequeue());
}
else
{
var tcs = new TaskCompletionSource<T>();
m_waiting.Enqueue(tcs);
return tcs.Task;
}
}
}
}
Public Class AsyncProducerConsumerCollection(Of T)
Private ReadOnly m_collection As New Queue(Of T)()
Private ReadOnly m_waiting As New Queue(Of TaskCompletionSource(Of T))()
Public Sub Add(item As T)
Dim tcs As TaskCompletionSource(Of T) = Nothing
SyncLock m_collection
If m_waiting.Count > 0 Then
tcs = m_waiting.Dequeue()
Else
m_collection.Enqueue(item)
End If
End SyncLock
If tcs IsNot Nothing Then tcs.TrySetResult(item)
End Sub
Public Function Take() As Task(Of T)
SyncLock m_collection
If m_collection.Count > 0 Then
Return Task.FromResult(m_collection.Dequeue())
Else
Dim tcs As New TaskCompletionSource(Of T)()
m_waiting.Enqueue(tcs)
Return tcs.Task
End If
End SyncLock
End Function
End Class
해당 데이터 구조를 사용하면 다음과 같은 코드를 작성할 수 있습니다.
static AsyncProducerConsumerCollection<int> m_data = new();
public static async Task ConsumerAsync()
{
while (true)
{
int nextItem = await m_data.Take();
Stubs.ProcessNextItem(nextItem);
}
}
public static void Produce(int data)
{
m_data.Add(data);
}
Private m_data As New AsyncProducerConsumerCollection(Of Integer)()
Public Async Function ConsumerAsync() As Task
While True
Dim nextItem As Integer = Await m_data.Take()
Stubs.ProcessNextItem(nextItem)
End While
End Function
Public Sub Produce(data As Integer)
m_data.Add(data)
End Sub
네임스페이스 System.Threading.Tasks.Dataflow에는 사용자 지정 컬렉션 형식을 만들 필요 없이 비슷한 방식으로 사용할 수 있는 BufferBlock<T> 형식이 포함됩니다.
static BufferBlock<int> m_dataBlock = new();
public static async Task ConsumerAsyncBlock()
{
while (true)
{
int nextItem = await m_dataBlock.ReceiveAsync();
Stubs.ProcessNextItem(nextItem);
}
}
public static void ProduceBlock(int data)
{
m_dataBlock.Post(data);
}
Private m_dataBlock As New BufferBlock(Of Integer)()
Public Async Function ConsumerAsyncBlock() As Task
While True
Dim nextItem As Integer = Await m_dataBlock.ReceiveAsync()
Stubs.ProcessNextItem(nextItem)
End While
End Function
Public Sub ProduceBlock(data As Integer)
m_dataBlock.Post(data)
End Sub
비고
System.Threading.Tasks.Dataflow 네임스페이스는 NuGet 패키지로 사용할 수 있습니다. 네임스페이스가 System.Threading.Tasks.Dataflow 포함된 어셈블리를 설치하려면 Visual Studio에서 프로젝트를 열고 프로젝트 메뉴에서 NuGet 패키지 관리를 선택하고 패키지를 온라인으로 System.Threading.Tasks.Dataflow 검색합니다.
참고하십시오
.NET