Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Ao usar o padrão assíncrono baseado em tarefas (TAP) para trabalhar com operações assíncronas, pode-se utilizar retornos de chamada para alcançar a espera sem bloquear. Para tarefas, este padrão utiliza métodos como Task.ContinueWith. O suporte assíncrono baseado em linguagem oculta retornos de chamada, permitindo que operações assíncronas sejam aguardadas dentro do fluxo de controle normal, e o código gerado pelo compilador fornece esse mesmo suporte em nível de API.
Suspendendo a execução com o Await
Você pode usar a palavra-chave await em C# e o Operador Await no Visual Basic para aguardar Task e Task<TResult> objetos de forma assíncrona. Quando espera um Task, a expressão await é do tipo void. Quando espera por um Task<TResult>, a expressão await é do tipo TResult. Uma await expressão deve ocorrer dentro do corpo de um método assíncrono. (Esses recursos de linguagem foram introduzidos no .NET Framework 4.5.)
Nos bastidores, a funcionalidade await instala um retorno de chamada na tarefa usando uma sequência. Este retorno de chamada retoma o método assíncrono no ponto de suspensão. Quando o método assíncrono é retomado, se a operação aguardada foi concluída com êxito e foi um Task<TResult>, o seu TResult é retornado. Se o Task ou Task<TResult> que era esperado se concluiu no estado Canceled, uma exceção OperationCanceledException é lançada. Se o Task ou Task<TResult> que era aguardado terminou no estado Faulted, a exceção que causou a falha é lançada. Uma Task pode falhar devido a várias exceções, mas apenas uma dessas exceções é propagada. No entanto, a Task.Exception propriedade retorna uma AggregateException exceção que contém todos os erros.
Se um contexto de sincronização (SynchronizationContext objeto) estiver associado ao thread que estiver a executar o método assíncrono no momento da suspensão (por exemplo, se a propriedade SynchronizationContext.Current não for null), o método assíncrono será retomado nesse mesmo contexto de sincronização usando o método Post do contexto. Caso contrário, ele depende do agendador de tarefas (TaskScheduler objeto) que estava atualizado no momento da suspensão. Normalmente, esse é o agendador de tarefas padrão (TaskScheduler.Default), que tem como alvo o pool de threads. Este agendador de tarefas determina se a operação assíncrona esperada deve ser retomada onde foi concluída ou se a retomada deve ser agendada. O agendador padrão normalmente permite que a continuação seja executada no thread que a operação aguardada foi concluída.
Quando você chama um método assíncrono, ele executa sincronizadamente o corpo da função até à primeira expressão await numa instância esperável que ainda não está completa, momento em que a invocação é devolvida ao chamador. Se o método assíncrono não devolver void, devolve um Task objeto ou Task<TResult> para representar o cálculo em curso. Em um método assíncrono não vazio RanToCompletion, se uma instrução return for encontrada ou o final do corpo do método for atingido, a tarefa conclui-se no estado final. Se uma exceção não tratada fizer com que o controlo saia do corpo do método assíncrono, a tarefa terminará no estado Faulted. Se essa exceção for um OperationCanceledException, a tarefa termina no estado Canceled. Desta forma, o resultado ou exceção é eventualmente publicado.
Existem várias variações importantes deste comportamento. Por razões de desempenho, se uma tarefa já estiver concluída quando a tarefa é aguardada, o controlo não é cedido e a função continua a ser executada. Além disso, regressar ao contexto original nem sempre é o comportamento desejado e pode ser alterado; Este comportamento é descrito com mais detalhe na secção seguinte.
Configurando a suspensão e a retomada com Yield e ConfigureAwait
Vários métodos fornecem mais controle sobre a execução de um método assíncrono. Por exemplo, você pode usar o Task.Yield método para introduzir um ponto de rendimento no método assíncrono:
public class Task : …
{
public static YieldAwaitable Yield();
…
}
Este método é equivalente a publicar ou agendar de forma assíncrona para o contexto atual.
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
Você também pode usar o método Task.ConfigureAwait para um melhor controle sobre a suspensão e a retomada em um método assíncrono. Como mencionado anteriormente, por padrão, o contexto atual é capturado no momento em que um método assíncrono é suspenso e esse contexto capturado é usado para invocar a continuação do método assíncrono após a retomada. Em muitos casos, este é o comportamento exato que você quer. Noutros casos, pode não se importar com o contexto de continuação, e pode obter melhor desempenho evitando que esses posts voltem ao contexto original. Para permitir este comportamento, use o método Task.ConfigureAwait para informar à operação await para não capturar e retomar no contexto, mas sim para continuar a execução onde a operação assíncrona que estava a ser aguardada foi concluída.
await someTask.ConfigureAwait(continueOnCapturedContext:false);
Awaitables, ConfigureAwait e SynchronizationContext
await funciona com qualquer tipo que satisfaça o padrão de expressão awaitable, não apenas Task. Um tipo é aguardável se fornecer um método GetAwaiter compatível que devolve um tipo com membros IsCompleted, OnCompleted e GetResult. Na maioria das APIs públicas, retorna Task, Task<TResult>, ValueTask, ou ValueTask<TResult>. Utilize awaitables personalizados exclusivamente em cenários especializados.
ConfigureAwait Use quando a continuação não precisa do contexto do chamador. No código de uma aplicação que atualiza uma interface, é frequentemente necessária captura de contexto. No código de biblioteca reutilizável, ConfigureAwait(false) é geralmente preferido porque evita saltos desnecessários de contexto e reduz o risco de deadlock para os chamadores que bloqueiam.
ConfigureAwait(false) altera o planeamento de continuação, não ExecutionContext o fluxo. Para uma explicação mais aprofundada do comportamento do contexto, veja ExecutionContext e SynchronizationContext.
Cancelamento de uma operação assíncrona
A partir do .NET Framework 4, os métodos TAP que suportam cancelamento fornecem pelo menos uma sobrecarga que aceita um token de cancelamento (CancellationToken objeto).
Através de uma fonte de token de cancelamento (objeto CancellationTokenSource), cria-se um token de cancelamento. A propriedade Token da fonte devolve o token de cancelamento que sinaliza quando o método Cancel da fonte é chamado.
var cts = new CancellationTokenSource();
string result = await DownloadStringTaskAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();
Por exemplo, se quiser descarregar uma única página web e quiser poder cancelar a operação, crie um CancellationTokenSource objeto, passe o seu token para o método TAP e depois chame o método da fonte Cancel quando estiver pronto para cancelar a operação:
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();
Ou, você pode passar o mesmo token para um subconjunto seletivo de operações:
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();
Importante
Qualquer tópico pode iniciar pedidos de cancelamento.
Pode passar o CancellationToken.None valor a qualquer método que aceite um token de cancelamento para indicar que o cancelamento nunca é solicitado. Este valor faz com que a CancellationToken.CanBeCanceled propriedade devolva false, e o método chamado pode otimizar em conformidade. Para fins de teste, você também pode passar um token de cancelamento pré-cancelado que é instanciado usando o construtor que aceita um valor Boolean para indicar se o token deve começar em um estado já cancelado ou não cancelável.
Esta abordagem ao cancelamento tem várias vantagens:
Você pode passar o mesmo token de cancelamento para qualquer número de operações assíncronas e síncronas.
O mesmo pedido de cancelamento pode ser enviado a qualquer número de ouvintes.
O programador da API assíncrona tem controlo total sobre se o cancelamento pode ser solicitado e quando entra em vigor.
O código que consome a API pode determinar seletivamente as invocações assíncronas para onde os pedidos de cancelamento são enviados.
Monitorização do progresso
Alguns métodos assíncronos expõem o progresso através de uma interface de acompanhamento que é passada para o método assíncrono. Por exemplo, considere uma função que baixa de forma assíncrona uma cadeia de caracteres de texto e, ao longo do caminho, gera atualizações de progresso que incluem a porcentagem do download concluído até agora. Pode consumir tal método numa aplicação Windows Presentation Foundation (WPF) da seguinte forma:
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; }
}
Utilização dos combinadores baseados em tarefas incorporados
O System.Threading.Tasks namespace inclui vários métodos para compor e trabalhar com tarefas.
Observação
Vários exemplos de código nesta secção utilizam Bitmap, que requer o pacote System.Drawing.Common e é suportado apenas em Windows. Os padrões assíncronos que eles demonstram aplicam-se a todas as plataformas; utilize uma biblioteca de imagem multiplataforma para plataformas que não sejam Windows.
Task.Run
A Task classe inclui vários Run métodos que permitem facilmente transferir trabalho como um Task ou Task<TResult> para o pool de threads. Por exemplo:
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
Alguns desses Run métodos, como a Task.Run(Func<Task>) sobrecarga, existem como abreviação para o TaskFactory.StartNew método. Esta sobrecarga permite-lhe usar await dentro do trabalho descarregado. Por exemplo:
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
Tais sobrecargas são logicamente equivalentes ao uso do TaskFactory.StartNew método em conjunto com o Unwrap método de extensão na Biblioteca Paralela de Tarefas.
Task.FromResult
Use o FromResult método em cenários onde os dados já possam estar disponíveis e apenas precisa de os devolver de um método de retorno de tarefas elevado para um 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
Tarefa.QuandoTodos
Use o método WhenAll para aguardar de forma assíncrona em várias operações assíncronas que são representadas como tarefas. O método tem múltiplas sobrecargas que suportam um conjunto de tarefas não genéricas ou um conjunto não uniforme de tarefas genéricas (por exemplo, esperar assíncronamente por múltiplas operações que retornam void, ou esperar assíncronamente por múltiplos métodos que retornam valores onde cada valor pode ter um tipo diferente) e também suportar um conjunto uniforme de tarefas genéricas (como esperar assíncronamente por múltiplos métodos que retornam TResult).
Suponha que quer enviar mensagens de email a vários clientes. Você pode sobrepor o envio das mensagens para não esperar que uma mensagem seja concluída antes de enviar a próxima. Também pode descobrir quando as operações de envio terminam e se ocorrem erros:
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);
Este código não lida explicitamente com exceções que possam ocorrer, mas permite que exceções se propaguem para fora da await tarefa resultante a partir de WhenAll. Para lidar com as exceções, utilize código como o seguinte:
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
Neste caso, se qualquer operação assíncrona falhar, todas as exceções são consolidadas numa AggregateException exceção, que é armazenada no Task que é devolvido pelo WhenAll método. No entanto, apenas uma dessas exceções é propagada pela palavra-chave await . Se você quiser examinar todas as exceções, você pode reescrever o código anterior da seguinte maneira:
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
Considere um exemplo de descarregar múltiplos ficheiros da web de forma assíncrona. Nesse caso, todas as operações assíncronas têm tipos de resultados homogêneos e é fácil acessar os resultados:
string [] pages = await Task.WhenAll(
from url in urls select DownloadStringTaskAsync(url));
Pode usar as mesmas técnicas de tratamento de exceções discutidas no cenário anterior de retorno de vazio:
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
Tarefa.QuandoQualquer
Use o WhenAny método para esperar assíncronamente apenas uma das múltiplas operações assíncronas representadas como tarefas para completar. Este método serve quatro casos de uso principais:
Redundância: Realizar uma operação várias vezes e selecionar a que é concluída primeiro (por exemplo, contactar vários serviços web de cotação de ações que devolvam um único resultado e selecionar aquele que se completa mais rapidamente).
Interleaving: Lançar várias operações e esperar que todas sejam concluídas, mas processá-las à medida que são concluídas.
Gestão de operações: Permitir que operações adicionais comecem à medida que outras são concluídas. Este cenário é uma extensão do cenário de entrelaçamento.
Resgate antecipado: Por exemplo, uma operação representada pela tarefa t1 pode ser agrupada na WhenAny tarefa com outra tarefa t2, e você pode esperar pela WhenAny tarefa. A tarefa t2 pode representar um tempo limite, um cancelamento ou algum outro sinal que faz com que a tarefa WhenAny seja concluída antes de t1 ser concluída.
Redundância
Considere um caso em que você quer tomar uma decisão sobre comprar uma ação. Existem vários serviços web de recomendação de stock em que confia, mas dependendo da carga diária, cada serviço pode acabar por ser lento em momentos diferentes. Use o WhenAny método para receber uma notificação quando qualquer operação estiver concluída:
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
Ao contrário de WhenAll, que retorna os resultados desembrulhados de todas as tarefas concluídas com êxito, WhenAny retorna a tarefa que foi concluída. Se uma tarefa falhar, é importante saber que falhou e, se uma tarefa for bem-sucedida, é importante saber a qual tarefa o valor de retorno está associado. Portanto, você precisa acessar o resultado da tarefa retornada, ou aguardar mais, como mostra este exemplo.
Tal como WhenAll, tem de ser capaz de acomodar exceções. Como você recebe a tarefa concluída de volta, pode aguardar que a tarefa retornada tenha erros identificados e gerí-los apropriadamente; por exemplo:
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
Além disso, mesmo que uma primeira tarefa seja concluída com sucesso, as tarefas seguintes podem falhar. Neste momento, tens várias opções para lidar com exceções: podes esperar que todas as tarefas lançadas estejam concluídas, caso em que podes usar o WhenAll método, ou podes decidir que todas as exceções são importantes e devem ser registadas. Neste cenário, pode usar continuações para receber uma notificação quando as tarefas são concluídas de forma assíncrona:
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => { if (t.IsFaulted) Log(t.Exception); });
}
ou:
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}
ou ainda:
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
Por fim, pode querer cancelar todas as operações restantes:
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
Intercalação
Considere um caso em que você está baixando imagens da Web e processando cada imagem (por exemplo, adicionando a imagem a um controle de interface do usuário). Você processa as imagens sequencialmente no thread da interface do usuário, mas deseja baixar as imagens o mais simultaneamente possível. Além disso, você não quer adiar a adição das imagens à interface do usuário até que todas sejam baixadas. Em vez disso, você deseja adicioná-los à medida que forem concluídos.
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
Você também pode aplicar o intercalamento a um cenário que envolve processamento computacionalmente intensivo nas imagens transferidas, por exemplo:
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
Regulação de Velocidade
Considere o exemplo da intercalação, exceto que o utilizador está a descarregar tantas imagens que os downloads têm de ser limitados. Por exemplo, queres que apenas um número específico de downloads ocorra em simultâneo. Para alcançar este objetivo, inicia-se um subconjunto das operações assíncronas. À medida que as operações são concluídas, você pode iniciar operações adicionais para substituí-las:
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
Resgate antecipado
Considere que você está aguardando de forma assíncrona a conclusão de uma operação e, ao mesmo tempo, responde à solicitação de cancelamento de um usuário (por exemplo, o usuário clicou em um botão de cancelamento). O código a seguir ilustra esse cenário:
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
Esta implementação reativa a interface do utilizador assim que decidir desistir, mas não cancela as operações assíncronas subjacentes. Outra alternativa seria solicitar o cancelamento das operações pendentes quando decidir interromper, mas não restabelecer a interface do utilizador até que as operações sejam concluídas, possivelmente com término antecipado devido ao pedido de cancelamento.
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
Outro exemplo de resgate antecipado envolve o uso do método WhenAny em conjunto com o método Delay, como discutido na próxima seção.
Task.Delay (atraso de tarefa)
Use o Task.Delay método para adicionar pausas à execução de um método assíncrono. Esta pausa é útil para muitos tipos de funcionalidades, incluindo a construção de ciclos de sondagem e o adiamento do tratamento da entrada do utilizador por um período de tempo predeterminado. Também pode usar o Task.Delay método com Task.WhenAny para implementar expirações de tempo em operações de espera.
Se uma tarefa que faz parte de uma operação assíncrona maior (por exemplo, um serviço Web ASP.NET) demorar muito para ser concluída, a operação geral poderá ser prejudicada, especialmente se não for concluída. Por esse motivo, é importante ser capaz de definir um tempo limite ao aguardar por uma operação assíncrona. Os métodos síncronos Task.Wait, Task.WaitAll e Task.WaitAny aceitam limites de tempo, mas os métodos correspondentes TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny e anteriormente mencionados Task.WhenAll/Task.WhenAny não. Em vez disso, usem Task.Delay e Task.WhenAny juntos para implementar um tempo de pausa.
Por exemplo, na sua aplicação de interface, suponha que quer descarregar uma imagem e desativar a interface enquanto a imagem está a ser descarregada. No entanto, se o download demorar muito, você deseja reativar a interface do usuário e descartar o download:
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
O mesmo princípio aplica-se a múltiplos downloads, porque WhenAll retorna uma tarefa:
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
Criando combinadores baseados em tarefas
Como uma tarefa é capaz de representar completamente uma operação assíncrona e fornecer recursos síncronos e assíncronos para unir à operação, recuperar seus resultados e assim por diante, você pode criar bibliotecas úteis de combinadores que compõem tarefas para criar padrões maiores. Conforme discutido na seção anterior, o .NET inclui vários combinadores internos, mas você também pode criar o seu próprio. As seções a seguir fornecem vários exemplos de possíveis métodos e tipos de combinadores.
RetryOnFault
Em muitas situações, queres tentar novamente uma operação se uma tentativa anterior falhar. Para código síncrono, pode construir um método auxiliar, como RetryOnFault no exemplo seguinte, para realizar esta tarefa:
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
Você pode criar um método auxiliar quase idêntico para operações assíncronas que são implementadas com TAP e, assim, retornar tarefas:
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
Pode então usar este combinador para codificar tentativas de repetição no código da aplicação. Por exemplo:
// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
() => DownloadStringTaskAsync(url), 3);
Pode prolongar ainda mais a RetryOnFault função. Por exemplo, a função pode aceitar outra Func<Task> que a função invoca entre tentativas para determinar quando tentar novamente a operação. Por exemplo:
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
Pode então usar a função da seguinte forma para esperar um segundo antes de tentar novamente a operação:
// 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
Às vezes, você pode aproveitar a redundância para melhorar a latência de uma operação e as chances de sucesso. Considere vários serviços web que fornecem cotações de ações, mas em diferentes momentos do dia, cada serviço pode oferecer diferentes níveis de qualidade e tempos de resposta. Para lidar com estas flutuações, pode enviar pedidos a todos os serviços web e, assim que receber uma resposta de um, cancelar os pedidos restantes. Você pode implementar uma função auxiliar para facilitar a implementação desse padrão comum de iniciar várias operações, aguardar qualquer uma e, em seguida, cancelar o restante. A NeedOnlyOne função no exemplo a seguir ilustra esse cenário:
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
Em seguida, você pode usar essa função da seguinte maneira:
double currentPrice = await NeedOnlyOne(
ct => GetCurrentPriceFromServer1Async("msft", ct),
ct => GetCurrentPriceFromServer2Async("msft", ct),
ct => GetCurrentPriceFromServer3Async("msft", ct));
Operações intercaladas
Usar o WhenAny método para suportar um cenário de entrelaçamento pode causar um problema de desempenho quando se trabalha com grandes conjuntos de tarefas. Cada chamada para WhenAny regista uma continuação com cada tarefa. Para N número de tarefas, este processo cria O(N2) continuações ao longo da vida útil da operação de entrelaçamento. Se estiver a trabalhar com um grande conjunto de tarefas, use um combinador (Interleaved no exemplo seguinte) para resolver o problema de desempenho:
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
Use o combinador para processar os resultados das tarefas à medida que são concluídas. Por exemplo:
IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
int result = await task;
…
}
WhenAllOrFirstException
Em certos cenários de dispersão/recolha, pode ser necessário esperar por todas as tarefas de um conjunto, a menos que uma delas falhe. Nesse caso, deve parar de esperar assim que a exceção ocorrer. Pode conseguir esse comportamento usando um método de combinador, como WhenAllOrFirstException no seguinte exemplo:
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
Construção de estruturas de dados baseadas em tarefas
Além da capacidade de criar combinadores personalizados baseados em tarefas, ter uma estrutura de dados em Task e Task<TResult> que representa os resultados de uma operação assíncrona e a sincronização necessária para se unir a ela o torna um tipo poderoso no qual construir estruturas de dados personalizadas para serem usadas em cenários assíncronos.
AsyncCache
Um aspeto importante de uma tarefa é que pode distribuí-la a vários consumidores. Todos os consumidores podem esperar por isto, registar continuações com ele, obter o seu resultado ou exceções (no caso de Task<TResult>), e assim sucessivamente. Este aspeto torna Task e Task<TResult> perfeitamente adequados para uso numa infraestrutura de cache assíncrona. Aqui está um exemplo de um cache assíncrono pequeno, mas poderoso, construído sobre 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
A classe AsyncCache<TKey,TValue> aceita como um delegado para seu construtor uma função que usa um TKey e retorna um Task<TResult>. O dicionário interno armazena quaisquer valores previamente acedidos da cache e AsyncCache garante que gera apenas uma tarefa por chave, mesmo que a cache seja acedida simultaneamente.
Por exemplo, você pode criar um cache para páginas da Web baixadas:
private AsyncCache<string,string> m_webPages =
new AsyncCache<string,string>(DownloadStringTaskAsync);
Em seguida, você pode usar esse cache em métodos assíncronos sempre que precisar do conteúdo de uma página da Web. A AsyncCache classe garante que você esteja baixando o menor número possível de páginas e armazena em cache os resultados.
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
Coleção Produtor-Consumidor Assíncrona
Você também pode usar tarefas para criar estruturas de dados para coordenar atividades assíncronas. Considere um dos padrões clássicos de design paralelo: produtor/consumidor. Neste padrão, os produtores geram dados que os consumidores consomem, e produtores e consumidores podem funcionar em paralelo. Por exemplo, o consumidor processa o item 1, que anteriormente era gerado por um produtor que agora está produzindo o item 2. Para o padrão produtor/consumidor, é sempre necessário algum tipo de estrutura de dados para armazenar o trabalho criado pelos produtores, para que os consumidores possam ser notificados de novos dados e encontrá-los quando disponíveis.
Aqui está uma estrutura de dados simples, construída sobre tarefas, que permite que métodos assíncronos sejam usados como produtores e consumidores:
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
Com essa estrutura de dados estabelecida, você pode escrever código como o seguinte:
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
O System.Threading.Tasks.Dataflow namespace inclui o BufferBlock<T> tipo, que você pode usar de maneira semelhante, mas sem precisar criar um tipo de coleção personalizado:
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
Observação
O System.Threading.Tasks.Dataflow namespace está disponível como um pacote NuGet. Para instalar o assembly que contém o System.Threading.Tasks.Dataflow namespace, abra seu projeto no Visual Studio, escolha Gerenciar Pacotes NuGet no menu Projeto e pesquise o System.Threading.Tasks.Dataflow pacote online.