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();
…
}
이는 현재 컨텍스트에 비동기적으로 게시하거나 다시 예약하는 것과 같습니다.
Task.Run(async delegate
{
for(int i=0; i<1000000; i++)
{
await Task.Yield(); // fork the continuation into a separate work item
...
}
});
또한 비동기 메서드에서 Task.ConfigureAwait 일시 중단 및 재개를 보다 효율적으로 제어하기 위해 이 메서드를 사용할 수 있습니다. 앞에서 설명한 것처럼 기본적으로 현재 컨텍스트는 비동기 메서드가 일시 중단될 때 캡처되며 캡처된 컨텍스트는 다시 시작 시 비동기 메서드의 연속 작업을 호출하는 데 사용됩니다. 대부분의 경우 이것이 원하는 정확한 동작입니다. 다른 경우에는 연속 컨텍스트에 대해 신경 쓰지 않을 수 있으며 이러한 게시물을 원래 컨텍스트로 다시 사용하지 않도록 하여 더 나은 성능을 얻을 수 있습니다. 이를 활성화하려면 Task.ConfigureAwait 메서드를 사용하여 대기 중인 비동기 작업이 완료된 이후에도 실행이 해당 컨텍스트에서 캡처 및 재개되지 않도록 알리고, 대신 비동기 작업이 완료된 위치에서 실행이 계속되도록 설정해야 합니다.
await someTask.ConfigureAwait(continueOnCapturedContext:false);
비동기 작업 취소
.NET Framework 4부터 취소를 지원하는 TAP 메서드는 취소 토큰(CancellationToken 개체)을 허용하는 오버로드를 하나 이상 제공합니다.
취소 토큰은 취소 토큰 원본(CancellationTokenSource 개체)을 통해 생성됩니다. Token 속성은 Cancel 메서드가 호출될 때 신호를 받을 취소 토큰을 반환합니다. 예를 들어 단일 웹 페이지를 다운로드하고 작업을 취소하려면 개체를 만들고 CancellationTokenSource 해당 토큰을 TAP 메서드에 전달한 다음 작업을 취소할 준비가 되면 소스 메서드 Cancel 를 호출합니다.
var cts = new CancellationTokenSource();
string result = await DownloadStringTaskAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.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를 사용하는 코드는 취소 요청이 전파될 비동기 호출을 선택적으로 결정할 수 있습니다.
진행률 모니터링
일부 비동기 메서드는 비동기 메서드에 전달된 진행률 인터페이스를 통해 진행률을 표시합니다. 예를 들어 텍스트 문자열을 비동기적으로 다운로드하고 그 과정에서 지금까지 완료된 다운로드의 백분율을 포함하는 진행률 업데이트를 발생시키는 함수를 고려해 보세요. 이러한 메서드는 다음과 같이 WPF(Windows Presentation Foundation) 애플리케이션에서 사용할 수 있습니다.
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 작업을 구성하고 작업하기 위한 몇 가지 방법이 포함되어 있습니다.
Task.Run (작업 실행)
클래스에는 몇 가지 Task 메서드가 포함되어 있어, 작업을 Run나 Task 형태로 스레드 풀에 쉽게 오프로드할 수 있습니다. 예를 들어:
public async void button1_Click(object sender, EventArgs e)
{
textBox1.Text = await Task.Run(() =>
{
// … do compute-bound work here
return answer;
});
}
이러한 Run 메서드 중 일부는 Task.Run(Func<Task>) 오버로드처럼 TaskFactory.StartNew 메서드의 단축형으로 존재합니다. 이 오버로드를 사용하면 오프로드된 작업 내에서 await를 사용할 수 있습니다. 예를 들면 다음과 같습니다.
public async void button1_Click(object sender, EventArgs e)
{
pictureBox1.Image = await Task.Run(async() =>
{
using(Bitmap bmp1 = await DownloadFirstImageAsync())
using(Bitmap bmp2 = await DownloadSecondImageAsync())
return Mashup(bmp1, bmp2);
});
}
이러한 오버로드는 작업 병렬 라이브러리에서 TaskFactory.StartNew 메서드를 Unwrap 확장 메서드와 함께 사용하는 것과 논리적으로 동일합니다.
Task.FromResult
FromResult 메서드를 데이터가 이미 있고 반환만 필요할 때, Task<TResult>에 포함된 작업 반환 메서드에서 사용하십시오.
public Task<int> GetValueAsync(string key)
{
int cachedValue;
return TryGetCachedValue(out cachedValue) ?
Task.FromResult(cachedValue) :
GetValueAsyncInternal();
}
private async Task<int> GetValueAsyncInternal(string key)
{
…
}
Task.WhenAll
WhenAll 메서드를 사용하여 작업으로 표시되는 여러 비동기 작업을 비동기적으로 대기합니다. 이 메서드에는 제네릭이 아닌 작업 집합 또는 비균일한 제네릭 작업 집합을 지원하기 위한 여러 오버로드가 있습니다. 예를 들어, 여러 void 반환 작업을 비동기적으로 대기하거나, 각 값이 서로 다른 형식일 수 있는 여러 값 반환 메서드를 비동기적으로 대기할 때 사용합니다. 또한, 이 메서드는 균일한 제네릭 작업 집합(예: 여러 TResult
반환 메서드를 비동기적으로 대기)을 지원합니다.
여러 고객에게 전자 메일 메시지를 보내려고 하는 경우를 가정해 보겠습니다. 메시지를 보내기 전에 한 메시지가 완료되기를 기다리지 않도록 메시지를 겹칠 수 있습니다. 송신 작업이 완료된 시기와 오류가 발생했는지 여부도 확인할 수 있습니다.
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);
이 코드는 발생할 수 있는 예외를 명시적으로 처리하지는 않지만, await
내부에서 발생한 예외가 WhenAll로부터의 결과 작업에 전파되도록 합니다. 예외를 처리하려면 다음과 같은 코드를 사용할 수 있습니다.
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
try
{
await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
...
}
이 경우, 비동기 작업 중 하나라도 실패할 경우, 모든 예외가 AggregateException 예외로 통합되며, 이는 Task 메서드에서 반환되는 WhenAll에 저장됩니다. 그러나 이러한 예외 중 하나만 await
키워드에 의해 전파됩니다. 모든 예외를 검사하려는 경우 다음과 같이 이전 코드를 다시 작성할 수 있습니다.
Task [] asyncOps = (from addr in addrs select SendMailAsync(addr)).ToArray();
try
{
await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
foreach(Task faulted in asyncOps.Where(t => t.IsFaulted))
{
… // work with faulted and faulted.Exception
}
}
웹에서 여러 파일을 비동기적으로 다운로드하는 예를 살펴보겠습니다. 이 경우 모든 비동기 작업에는 같은 유형의 결과 형식이 있으며 결과에 쉽게 액세스할 수 있습니다.
string [] pages = await Task.WhenAll(
from url in urls select DownloadStringTaskAsync(url));
이전 void 반환 시나리오에서 설명한 것과 동일한 예외 처리 기술을 사용할 수 있습니다.
Task<string> [] asyncOps =
(from url in urls select DownloadStringTaskAsync(url)).ToArray();
try
{
string [] pages = await Task.WhenAll(asyncOps);
...
}
catch(Exception exc)
{
foreach(Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
{
… // work with faulted and faulted.Exception
}
}
Task.WhenAny
이 메서드를 WhenAny 사용하여 태스크로 표시되는 여러 비동기 작업 중 하나만 완료될 때까지 비동기적으로 기다릴 수 있습니다. 이 메서드는 네 가지 기본 사용 사례를 제공합니다.
중복성: 작업을 여러 번 수행하고 먼저 완료하는 작업을 선택합니다(예: 단일 결과를 생성하는 여러 주식 견적 웹 서비스에 연결하고 가장 빠른 결과를 완료하는 웹 서비스 선택).
인터리빙: 여러 작업을 시작하고 모든 작업이 완료되기를 기다리지만, 완료될 때마다 처리합니다.
제한: 다른 작업이 완료될 때 추가 작업을 시작할 수 있도록 허용합니다. 인터리빙 시나리오의 확장입니다.
조기 구제: 예를 들어 작업 t1로 표시되는 작업은 다른 작업 t2를 사용하여 WhenAny 작업에서 그룹화할 수 있으며, WhenAny 작업을 기다릴 수 있습니다. 작업 t2는 t1이 완료되기 전에 WhenAny 작업이 완료되도록 하는 시간 초과, 취소, 또는 다른 신호를 나타낼 수 있습니다.
중복성
주식을 구입할지 여부를 결정하려는 경우를 고려합니다. 신뢰할 수 있는 몇 가지 재고 추천 웹 서비스가 있지만 일일 부하에 따라 각 서비스가 서로 다른 시간에 느려질 수 있습니다. 작업이 완료되면 이 WhenAny 메서드를 사용하여 알림을 받을 수 있습니다.
var recommendations = new List<Task<bool>>()
{
GetBuyRecommendation1Async(symbol),
GetBuyRecommendation2Async(symbol),
GetBuyRecommendation3Async(symbol)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
if (await recommendation) BuyStock(symbol);
성공적으로 완료된 모든 작업의 래핑되지 않은 결과를 반환하는 WhenAll와 달리, WhenAny는 완료된 작업을 반환합니다. 작업이 실패하는 경우 작업이 실패했음을 알아야 하며, 작업이 성공하면 반환 값이 연결된 작업을 알고 있는 것이 중요합니다. 따라서 반환된 작업의 결과에 액세스하거나 이 예제와 같이 추가로 대기해야 합니다.
마찬가지로 WhenAll예외를 수용할 수 있어야 합니다. 완료된 작업을 다시 받으면, 반환된 태스크에서 오류가 전파되기를 기다렸다가 적절하게 처리할 수 있습니다. 예를 들어 다음과 같습니다.
Task<bool> [] recommendations = …;
while(recommendations.Count > 0)
{
Task<bool> recommendation = await Task.WhenAny(recommendations);
try
{
if (await recommendation) BuyStock(symbol);
break;
}
catch(WebException exc)
{
recommendations.Remove(recommendation);
}
}
또한 첫 번째 작업이 성공적으로 완료되더라도 후속 작업이 실패할 수 있습니다. 이 시점에서 예외를 처리하기 위한 몇 가지 옵션이 있습니다. 시작된 모든 작업이 완료될 때까지 기다릴 수 있으며, 이 경우 메서드를 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) { Log(exc); }
}
}
…
LogCompletionIfFailed(recommendations);
마지막으로, 나머지 작업을 모두 취소할 수 있습니다.
var cts = new CancellationTokenSource();
var recommendations = new List<Task<bool>>()
{
GetBuyRecommendation1Async(symbol, cts.Token),
GetBuyRecommendation2Async(symbol, cts.Token),
GetBuyRecommendation3Async(symbol, cts.Token)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
cts.Cancel();
if (await recommendation) BuyStock(symbol);
인터리빙
웹에서 이미지를 다운로드하고 각 이미지를 처리하는 경우를 고려합니다(예: UI 컨트롤에 이미지 추가). UI 스레드에서 이미지를 순차적으로 처리하지만 가능한 한 동시에 이미지를 다운로드하려고 합니다. 또한 이미지를 모두 다운로드할 때까지 기다리지 않고 UI에 추가하고 싶을 것입니다. 대신 완료할 때마다 추가하려고 합니다.
List<Task<Bitmap>> imageTasks =
(from imageUrl in urls select GetBitmapAsync(imageUrl)).ToList();
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
panel.AddImage(image);
}
catch{}
}
또한 다운로드한 이미지에 대한 계산 집약적인 처리를 포함하는 시나리오에 ThreadPool 인터리빙을 적용할 수 있습니다. 예를 들면 다음과 같습니다.
List<Task<Bitmap>> imageTasks =
(from imageUrl in urls select GetBitmapAsync(imageUrl)
.ContinueWith(t => ConvertImage(t.Result)).ToList();
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
panel.AddImage(image);
}
catch{}
}
스로틀링
사용자가 너무 많은 이미지를 다운로드할 때 인터리빙 예제를 고려하십시오. 이 경우, 다운로드를 제한하여 한 번에 특정 수의 다운로드만 진행되도록 해야 합니다. 이를 위해 비동기 작업의 하위 집합을 시작할 수 있습니다. 작업이 완료되면 추가 작업을 시작하여 해당 작업을 수행할 수 있습니다.
const int CONCURRENCY_LEVEL = 15;
Uri [] urls = …;
int nextIndex = 0;
var imageTasks = new List<Task<Bitmap>>();
while(nextIndex < CONCURRENCY_LEVEL && nextIndex < urls.Length)
{
imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
nextIndex++;
}
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
panel.AddImage(image);
}
catch(Exception exc) { Log(exc); }
if (nextIndex < urls.Length)
{
imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
nextIndex++;
}
}
조기 구제 금융
사용자의 취소 요청에 동시에 응답하는 동안 작업이 완료되기를 비동기적으로 기다리는 것을 고려합니다(예: 사용자가 취소 단추를 클릭). 다음 코드에서는 이 시나리오를 보여 줍니다.
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();
btnRun.Enabled = false;
try
{
Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text);
await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
if (imageDownload.IsCompleted)
{
Bitmap image = await imageDownload;
panel.AddImage(image);
}
else imageDownload.ContinueWith(t => Log(t));
}
finally { btnRun.Enabled = true; }
}
private static async Task UntilCompletionOrCancellation(
Task asyncOp, CancellationToken ct)
{
var tcs = new TaskCompletionSource<bool>();
using(ct.Register(() => tcs.TrySetResult(true)))
await Task.WhenAny(asyncOp, tcs.Task);
return asyncOp;
}
이 구현은 구제하기로 결정하는 즉시 사용자 인터페이스를 다시 사용하도록 설정하지만 기본 비동기 작업을 취소하지는 않습니다. 또 다른 대안은 취소 요청으로 인해 일찍 종료될 수 있으므로 작업이 완료될 때까지 사용자 인터페이스를 다시 설정하지 않고 보류 중인 작업을 취소하는 것입니다.
private CancellationTokenSource m_cts;
public async void btnRun_Click(object sender, EventArgs e)
{
m_cts = new CancellationTokenSource();
btnRun.Enabled = false;
try
{
Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text, m_cts.Token);
await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
Bitmap image = await imageDownload;
panel.AddImage(image);
}
catch(OperationCanceledException) {}
finally { btnRun.Enabled = true; }
}
초기 구제의 또 다른 예는 다음 섹션에서 논의된 바와 같이 WhenAny 메서드와 Delay 메서드를 함께 사용하는 것입니다.
작업 지연
이 메서드를 Task.Delay 사용하여 비동기 메서드의 실행에 일시 중지를 도입할 수 있습니다. 이 기능은 폴링 루프를 빌드하고 미리 결정된 기간 동안 사용자 입력 처리를 지연하는 등 다양한 종류의 기능에 유용합니다. 이 Task.Delay 메서드는 awaits에서 시간 초과를 구현할 때 Task.WhenAny와 함께 사용할 수 있어 유용할 수 있습니다.
더 큰 비동기 작업의 일부인 작업(예: ASP.NET 웹 서비스)을 완료하는 데 너무 오래 걸리는 경우 전체 작업이 완료되지 않을 경우 특히 문제가 발생할 수 있습니다. 이러한 이유로 비동기 작업을 대기할 때 타임아웃을 설정할 수 있어야 합니다. 동기 Task.Wait및 Task.WaitAllTask.WaitAny 메서드는 제한 시간 값을 허용하지만, 해당 TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny 메서드와 앞에서 언급한 메서드는 Task.WhenAll/Task.WhenAny 그렇지 않습니다. 대신 시간 초과를 구현하기 위해 Task.Delay와 Task.WhenAny를 함께 사용할 수 있습니다.
예를 들어 UI 애플리케이션에서 이미지를 다운로드하고 이미지를 다운로드하는 동안 UI를 사용하지 않도록 설정하려는 경우를 가정해 보겠습니다. 그러나 다운로드 시간이 너무 오래 걸리는 경우 UI를 다시 사용하도록 설정하고 다운로드를 취소하려고 합니다.
public async void btnDownload_Click(object sender, EventArgs e)
{
btnDownload.Enabled = false;
try
{
Task<Bitmap> download = GetBitmapAsync(url);
if (download == await Task.WhenAny(download, Task.Delay(3000)))
{
Bitmap bmp = await download;
pictureBox.Image = bmp;
status.Text = "Downloaded";
}
else
{
pictureBox.Image = null;
status.Text = "Timed out";
var ignored = download.ContinueWith(
t => Trace("Task finally completed"));
}
}
finally { btnDownload.Enabled = true; }
}
작업을 반환하기 때문에 WhenAll 여러 다운로드에도 동일하게 적용됩니다.
public async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.Enabled = false;
try
{
Task<Bitmap[]> downloads =
Task.WhenAll(from url in urls select GetBitmapAsync(url));
if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
{
foreach(var bmp in downloads.Result) panel.AddImage(bmp);
status.Text = "Downloaded";
}
else
{
status.Text = "Timed out";
downloads.ContinueWith(t => Log(t));
}
}
finally { btnDownload.Enabled = true; }
}
작업 기반 조합기 구축
태스크는 비동기 작업을 완전히 나타내고 작업과 조인하고 결과를 검색하는 동기 및 비동기 기능을 제공할 수 있으므로 작업을 구성하는 결합자의 유용한 라이브러리를 빌드하여 더 큰 패턴을 작성할 수 있습니다. 이전 섹션에서 설명한 것처럼 .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);
}
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);
}
그런 다음 이 결합자를 사용하여 애플리케이션의 논리로 재시도를 인코딩할 수 있습니다. 예를 들어:
// 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> RetryOnFault<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);
}
그런 다음 다음과 같이 함수를 사용하여 작업을 다시 시도하기 전에 잠시 기다릴 수 있습니다.
// 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(
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 => Log(t), TaskContinuationOptions.OnlyOnFaulted);
}
return completed;
}
그런 다음 다음과 같이 이 함수를 사용할 수 있습니다.
double currentPrice = await NeedOnlyOne(
ct => GetCurrentPriceFromServer1Async("msft", ct),
ct => GetCurrentPriceFromServer2Async("msft", ct),
ct => GetCurrentPriceFromServer3Async("msft", ct));
교차 작업
대규모 작업 집합으로 작업할 때 WhenAny 메서드를 사용하여 인터리빙 시나리오를 지원하는 경우 잠재적인 성능 문제가 있을 수 있습니다.
WhenAny에 대한 호출마다 연속 작업이 각 작업에 등록됨을 유발합니다. N개 태스크의 경우 인터리빙 작업의 수명 동안 O(N2) 연속 작업이 생성됩니다. 대규모 작업 집합으로 작업하는 경우 다음 예제에서 조합기를Interleaved
사용하여 성능 문제를 해결할 수 있습니다.
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;
}
그런 다음, 조합기를 사용하여 작업이 완료되는 동안 작업의 결과를 처리할 수 있습니다. 예를 들어:
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 => t.Result).ToArray());
};
foreach (var t in inputs) t.ContinueWith(onCompleted);
return tcs.Task;
}
작업 기반 데이터 구조 빌드
사용자 지정 작업 기반 결합자를 구축하는 기능 외에도, Task 및 Task<TResult>가 비동기 작업의 결과와 그 작업과의 동기화에 필요한 요소들을 모두 나타내면 이는 비동기 시나리오에서 사용할 사용자 지정 데이터 구조를 구축하기에 강력한 데이터 구조가 됩니다.
비동기 캐시
작업의 중요한 측면 중 하나는 이 작업이 여러 소비자에게 배포될 수 있으며, 각각이 이를 기다리고 관련 작업을 계속 등록하며 결과 또는 예외(예: Task<TResult>의 경우)를 받을 수 있다는 점입니다. 이렇게 하면 Task 비동기 캐싱 인프라에서 사용할 수 있으며 Task<TResult> 완벽하게 적합합니다. 다음은 Task<TResult> 위에 구축된 작지만 강력한 비동기 캐시의 예시입니다.
public class AsyncCache<TKey, TValue>
{
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("valueFactory");
_valueFactory = valueFactory;
_map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
}
public Task<TValue> this[TKey key]
{
get
{
if (key == null) throw new ArgumentNullException("key");
return _map.GetOrAdd(key, toAdd =>
new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
}
}
}
AsyncCache<TKey,TValue> 클래스는 생성자에 대한 대리자로서 TKey
를 입력으로 받고 Task<TResult>를 반환하는 함수를 수락합니다. 캐시에서 이전에 액세스한 값은 내부 사전에 저장되며 AsyncCache
, 캐시가 동시에 액세스되더라도 키당 하나의 작업만 생성됩니다.
예를 들어 다운로드한 웹 페이지에 대한 캐시를 빌드할 수 있습니다.
private AsyncCache<string,string> m_webPages =
new AsyncCache<string,string>(DownloadStringTaskAsync);
그런 다음 웹 페이지의 콘텐츠가 필요할 때마다 비동기 메서드에서 이 캐시를 사용할 수 있습니다. 클래스는 AsyncCache
가능한 한 적은 수의 페이지를 다운로드하고 결과를 캐시합니다.
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.IsEnabled = false;
try
{
txtContents.Text = await m_webPages["https://www.microsoft.com"];
}
finally { btnDownload.IsEnabled = true; }
}
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;
}
}
}
}
해당 데이터 구조를 사용하면 다음과 같은 코드를 작성할 수 있습니다.
private static AsyncProducerConsumerCollection<int> m_data = …;
…
private static async Task ConsumerAsync()
{
while(true)
{
int nextItem = await m_data.Take();
ProcessNextItem(nextItem);
}
}
…
private static void Produce(int data)
{
m_data.Add(data);
}
네임스페이스 System.Threading.Tasks.Dataflow에는 사용자 지정 컬렉션 형식을 만들 필요 없이 비슷한 방식으로 사용할 수 있는 BufferBlock<T> 형식이 포함됩니다.
private static BufferBlock<int> m_data = …;
…
private static async Task ConsumerAsync()
{
while(true)
{
int nextItem = await m_data.ReceiveAsync();
ProcessNextItem(nextItem);
}
}
…
private static void Produce(int data)
{
m_data.Post(data);
}
비고
System.Threading.Tasks.Dataflow 네임스페이스는 NuGet 패키지로 사용할 수 있습니다. 네임스페이스가 System.Threading.Tasks.Dataflow 포함된 어셈블리를 설치하려면 Visual Studio에서 프로젝트를 열고 프로젝트 메뉴에서 NuGet 패키지 관리를 선택하고 패키지를 온라인으로 System.Threading.Tasks.Dataflow
검색합니다.
참고하십시오
.NET