Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Cuando utilizas el Patrón Asincrónico Basado en Tareas (TAP) para trabajar con operaciones asincrónicas, puedes emplear callbacks para lograr una espera sin bloqueo. En el caso de las tareas, este patrón usa métodos como Task.ContinueWith. El soporte asincrónico basado en lenguaje oculta los callbacks al permitir que las operaciones asincrónicas se esperen dentro del flujo de control normal, y el código generado por el compilador proporciona este mismo soporte a nivel de API.
Suspender la ejecución con Await
Puede usar la palabra clave await en C# y el operador Await en Visual Basic para esperar Task y Task<TResult> objetos de forma asincrónica. Cuando aguarda un Task, la expresión await es de tipo void. Cuando esperas un Task<TResult>, la expresión await es de tipo TResult. Debe producirse una expresión await dentro del cuerpo de un método asincrónico. (Estas características de lenguaje se introdujeron en .NET Framework 4.5).
En realidad, la funcionalidad de await instala una devolución de llamada en la tarea mediante una continuación. Esta devolución de llamada reanuda el método asincrónico en el punto de suspensión. Cuando se reanuda el método asincrónico, si la operación de espera finalizó correctamente y fue un objeto Task<TResult>, se devuelve su TResult. Si las clases Task o Task<TResult> por la que esperaba finalizaron con el estado Canceled, se produce una excepción OperationCanceledException. Si las clases Task o Task<TResult> por la que esperaba finalizaron con el estado Faulted, se produce la excepción que causó el error. Un objeto Task puede producir un error como resultado de múltiples excepciones, pero solo una de estas excepciones se propaga. Sin embargo, la Task.Exception propiedad devuelve una AggregateException excepción que contiene todos los errores.
Si un contexto de sincronización (objeto SynchronizationContext) está asociado con el subproceso que ejecutaba el método asincrónico en el momento de la suspensión (por ejemplo, si la propiedad SynchronizationContext.Current no es null), el método asincrónico se reanuda en ese mismo contexto de sincronización con el método Post del contexto. De lo contrario, se basa en el programador de tareas (objeto TaskScheduler) que estaba vigente en el momento de la suspensión. Normalmente, este es el programador de tareas predeterminado (TaskScheduler.Default), que tiene como destino el grupo de subprocesos. Este programador de tareas determina si la operación asincrónica esperada debe reanudarse donde se completó o si se debe programar la reanudación. El programador predeterminado normalmente permite que la continuación se ejecute en el hilo donde finalizó la operación en espera.
Cuando se llama a un método asincrónico, ejecuta sincrónicamente el cuerpo de la función hasta la primera expresión 'await' en una instancia 'awaitable' que aún no está completa, momento en el cual la invocación regresa al llamador. Si el método asincrónico no devuelve void, devuelve un Task objeto o Task<TResult> para representar el cálculo en curso. En un método asincrónico distinto de void, si se encuentra una instrucción de devolución o se alcanza el final del cuerpo del método, la tarea se completa en el estado final RanToCompletion. Si una excepción no controlada hace que el control salga del cuerpo del método asíncrono, la tarea finaliza en el estado Faulted. Si esa excepción es un OperationCanceledException, entonces la tarea finaliza en el estado Canceled. De esta manera, el resultado o la excepción se publican finalmente.
Existen varias variaciones importantes de este comportamiento. Por motivos de rendimiento, si una tarea ya se ha completado cuando se espera la tarea, no se cede el control y la función continúa ejecutándose. Además, volver al contexto original no siempre es el comportamiento deseado y se puede cambiar; este comportamiento se describe con más detalle en la sección siguiente.
Configuración de la suspensión y reanudación con Yield y ConfigureAwait
Varios métodos proporcionan más control sobre la ejecución de un método asincrónico. Por ejemplo, puede usar el Task.Yield método para introducir un punto de rendimiento en el método asincrónico:
public class Task : …
{
public static YieldAwaitable Yield();
…
}
Este método es equivalente a publicar o programar de forma asincrónica en el contexto actual.
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
También puede usar el Task.ConfigureAwait método para controlar mejor la suspensión y reanudación en un método asincrónico. Como se mencionó anteriormente, de forma predeterminada, el contexto actual se captura en el momento en que se suspende un método asincrónico y ese contexto capturado se usa para invocar la continuación del método asincrónico tras la reanudación. En muchos casos, este es el comportamiento exacto que desea. En otros casos, es posible que no le interese el contexto de continuación y puede lograr un mejor rendimiento evitando estas publicaciones de vuelta al contexto original. Para habilitar este comportamiento, utilice el método Task.ConfigureAwait para indicar a la operación await que no capture ni se reanude en el contexto, sino que continúe la ejecución en el punto donde se complete la operación asincrónica que estaba siendo esperada.
await someTask.ConfigureAwait(continueOnCapturedContext:false);
Awaitables, ConfigureAwait y SynchronizationContext
await funciona con cualquier tipo que satisfaga el patrón de expresión que se puede esperar, no solo Task. Un tipo es awaitable si proporciona un método compatible GetAwaiter que devuelve un tipo con los miembros IsCompleted, OnCompleted y GetResult. En la mayoría de las API públicas, devuelve Task, Task<TResult>, ValueTasko ValueTask<TResult>. Recomiende el uso de awaitables personalizados solo para escenarios especializados.
Utilice ConfigureAwait cuando la continuación no necesite el contexto del llamador. En el código de la aplicación que actualiza una interfaz de usuario, la captura de contexto suele ser necesaria. En el código de biblioteca reutilizable, ConfigureAwait(false) normalmente se prefiere porque evita saltos de contexto innecesarios y reduce el riesgo de interbloqueo para los autores de llamadas que bloquean.
ConfigureAwait(false) cambia la programación de continuación, no el flujo de ExecutionContext. Para obtener una explicación más detallada del comportamiento del contexto, consulte ExecutionContext y SynchronizationContext.
Cancelación de una operación asincrónica
A partir de .NET Framework 4, los métodos TAP que admiten la cancelación proporcionan al menos una sobrecarga que acepta un token de cancelación (CancellationToken objeto).
Se crea un token de cancelación mediante un objeto de origen de token de cancelación (CancellationTokenSource). La propiedad del Token origen devuelve el token de cancelación que indica cuándo se llama al método del Cancel origen.
var cts = new CancellationTokenSource();
string result = await DownloadStringTaskAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();
Por ejemplo, si desea descargar una sola página web y desea poder cancelar la operación, crear un CancellationTokenSource objeto, pasar su token al método TAP y, a continuación, llamar al método del Cancel origen cuando esté listo para cancelar la operación:
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();
O bien, puede pasar el mismo token a un subconjunto selectivo de operaciones:
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
Cualquiera de los subprocesos puede iniciar solicitudes de cancelación.
Puede pasar el CancellationToken.None valor a cualquier método que acepte un token de cancelación para indicar que nunca se solicita la cancelación. Este valor hace que la CancellationToken.CanBeCanceled propiedad devuelva falsey el método llamado puede optimizar en consecuencia. Con fines de prueba, también puede pasar un token de cancelación cancelado previamente cuyas instancias se crean mediante el constructor que acepta un valor booleano para indicar si el token debe iniciarse en un estado ya cancelado o que no se puede cancelar.
Este enfoque para la cancelación tiene varias ventajas:
Puede pasar el mismo token de cancelación a cualquier número de operaciones asincrónicas y sincrónicas.
La misma solicitud de cancelación puede ir a un número cualquiera de escuchadores.
El desarrollador de la API asincrónica tiene control total sobre si se puede solicitar la cancelación y cuándo surte efecto.
El código que consume la API puede determinar selectivamente las invocaciones asincrónicas a las que van las solicitudes de cancelación.
Supervisar el progreso
Algunos métodos asincrónicos manifiestan el progreso a través de una interfaz de progreso, que se le pasa al método asincrónico. Por ejemplo, considere una función que descarga de forma asincrónica una cadena de texto y, a lo largo del proceso, genera actualizaciones de progreso que incluyen el porcentaje de la descarga que se ha completado hasta ahora. Puede consumir este método en una aplicación de Windows Presentation Foundation (WPF) como se indica a continuación:
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; }
}
Uso de los combinadores integrados basados en tareas
El espacio de nombres System.Threading.Tasks incluye varios métodos para crear tareas y trabajar con ellas.
Nota:
Varios ejemplos de código de esta sección usan
Task.Run
La Task clase incluye varios Run métodos que permiten que delegues fácilmente el trabajo como Task o Task<TResult> en el grupo de subprocesos. Por ejemplo:
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
Algunos de estos métodos Run, como la sobrecarga Task.Run(Func<Task>), existen como forma abreviada del método TaskFactory.StartNew. Esta sobrecarga le permite usar await dentro del trabajo descargado. Por ejemplo:
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
Estas sobrecargas son lógicamente equivalentes a usar el TaskFactory.StartNew método junto con el Unwrap método de extensión en la biblioteca paralela de tareas.
Task.FromResult
Utilice el método FromResult en escenarios donde los datos ya puedan estar disponibles y solo necesite devolverlos desde un método que devuelve tareas incorporado dentro de un 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
Utilice el método WhenAll para esperar asincrónicamente en varias operaciones asincrónicas que se representan como tareas. El método tiene varias sobrecargas que admiten un conjunto de tareas no genéricas o un conjunto no uniforme de tareas genéricas (por ejemplo, esperar de forma asincrónica a varias operaciones sin valor de retorno o esperar de forma asincrónica a varios métodos que devuelven valores donde cada tipo de valor puede ser diferente) y admitir un conjunto uniforme de tareas genéricas (como esperar de forma asincrónica a varios métodos que devuelven TResult).
Supongamos que desea enviar mensajes de correo electrónico a varios clientes. Puede superponer el envío de los mensajes para que no espere a que se complete un mensaje antes de enviar el siguiente. También puede averiguar cuándo se completan las operaciones de envío y si se producen errores:
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);
Este código no controla explícitamente las excepciones que pueden producirse, pero permite que las excepciones se propaguen fuera de await en la tarea resultante de WhenAll. Para controlar las excepciones, use código como el siguiente:
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
En este caso, si se produce un error en alguna operación asincrónica, todas las excepciones se consolidan en una AggregateException excepción, que se almacena en el Task que se devuelve del WhenAll método . Sin embargo, solo una de esas excepciones es propagada por la palabra clave await. Si desea examinar todas las excepciones, puede volver a escribir el código anterior de la siguiente manera:
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 un ejemplo de descarga de varios archivos desde la web de forma asincrónica. En este caso, todas las operaciones asincrónicas tienen tipos de resultados homogéneos y es fácil acceder a los resultados:
string [] pages = await Task.WhenAll(
from url in urls select DownloadStringTaskAsync(url));
Puede usar las mismas técnicas de control de excepciones que se describen en el escenario anterior de devolución de 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
Utiliza el WhenAny método para esperar de manera asincrónica a que solo una de las múltiples operaciones asincrónicas representadas como tareas se complete. Este método sirve cuatro casos de uso principales:
Redundancia: realizar una operación varias veces y seleccionar la que se completa primero (por ejemplo, ponerse en contacto con varios servicios web de cotizaciones que devuelven un único resultado y seleccionar el que completa la más rápida).
Intercalación: iniciar varias operaciones y esperar que se completen todas, pero procesarlas a medida que se completan.
Limitación: permitir que operaciones adicionales comiencen a medida que otras se completan. Este escenario es una extensión del escenario de intercalación.
Recursividad temprana: por ejemplo, una operación representada por la tarea t1 puede agruparse en una tarea WhenAny con otra tarea t2, y puede esperar a la tarea WhenAny. La tarea t2 podría representar un tiempo de espera o una cancelación, o alguna otra señal que hace que la WhenAny tarea se complete antes de que se complete t1.
Redundancia
Considere un caso en el que quiera tomar una decisión sobre si desea comprar una acción. Existen varios servicios web de recomendación de existencias que confía, pero en función de la carga diaria, cada servicio puede acabar siendo lento en momentos diferentes. Use el WhenAny método para recibir una notificación cuando se complete cualquier operación:
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
A diferencia WhenAllde , que devuelve los resultados desencapsulados de todas las tareas que se completaron correctamente, WhenAny devuelve la tarea que se completó. Si se produce un error en una tarea, es importante saber que se produjo un error y, si una tarea se realiza correctamente, es importante saber a qué tarea está asociada el valor devuelto. Por lo tanto, debe tener acceso al resultado de la tarea devuelta o esperarla aún más, como se muestra en este ejemplo.
Al igual que con WhenAll, tiene que ser capaz de dar cabida a excepciones. Dado que recibe de vuelta la tarea de completa, puede esperar que se hayan propagado los errores en la tarea devuelta y try/catch adecuadamente; por ejemplo:
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
Además, incluso si una primera tarea se completa correctamente, es posible que se produzca un error en las tareas posteriores. En este momento, tiene varias opciones para tratar las excepciones: puede esperar hasta que se completen todas las tareas iniciadas, en cuyo caso puede usar el WhenAll método o puede decidir que todas las excepciones son importantes y deben registrarse. En este escenario, puede usar continuaciones para recibir una notificación cuando las tareas se completen de forma asincrónica:
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => { if (t.IsFaulted) Log(t.Exception); });
}
o:
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}
o incluso:
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 último, es posible que quiera cancelar todas las operaciones 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
Intercalación
Considere un caso en el que va a descargar imágenes de la web y procesar cada imagen (por ejemplo, agregar la imagen a un control de interfaz de usuario). Procesa las imágenes secuencialmente en el subproceso de interfaz de usuario, pero quiere descargar las imágenes lo más simultáneamente como sea posible. Además, no quiere retrasar la adición de imágenes a la interfaz hasta que se hayan descargado todas. En su lugar, desea agregarlas a medida que se completan.
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
También puede aplicar la intercalación en un escenario que implica el procesamiento de cálculo intensivo en la clase ThreadPool de las imágenes descargadas; por ejemplo:
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
Limitaciones
Considere el ejemplo de intercalación, excepto que el usuario está descargando tantas imágenes que se deben limitar las descargas. Por ejemplo, solo quiere que se produzcan simultáneamente un número específico de descargas. Para lograr este objetivo, inicie un subconjunto de las operaciones asincrónicas. A medida que se completen las operaciones, puede iniciar operaciones adicionales para ocupar su lugar.
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
Salida anticipada
Tenga en cuenta que espera de forma asincrónica para que se complete una operación mientras responde simultáneamente a la solicitud de cancelación de un usuario (por ejemplo, el usuario ha clic en un botón cancelar). En el código siguiente se muestra este escenario:
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 implementación permite volver a la interfaz de usuario en cuanto decide abandonar la operación, pero no se cancelan las operaciones asincrónicas subyacentes. Otra alternativa sería cancelar las operaciones pendientes cuando decide abandone la operación, pero no se restablece la interfaz de usuario hasta que las operaciones se hayan finalizado, posiblemente debido a una finalización anticipada debido a la solicitud de cancelación:
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
Otro ejemplo de rescate anticipado implica el uso del WhenAny método junto con el Delay método , como se describe en la sección siguiente.
Task.Delay (Retraso de tarea)
Use el Task.Delay método para agregar pausas a la ejecución de un método asincrónico. Esta pausa es útil para muchos tipos de funcionalidad, incluida la creación de bucles de sondeo y el retraso en la gestión de la entrada del usuario por un período predeterminado. También puede usar el Task.Delay método con Task.WhenAny para implementar tiempos de espera en espera.
Si una tarea que forma parte de una operación asincrónica mayor (por ejemplo, un servicio web de ASP.NET) tarda demasiado tiempo en completarse, la operación general podría sufrir, especialmente si no se completa alguna vez. Por este motivo, es importante poder agotar el tiempo de espera al esperar a una operación asincrónica. Los métodos sincrónicos Task.Wait, Task.WaitAll y Task.WaitAny aceptan valores de tiempo de espera, pero los métodos correspondientes TaskFactory.ContinueWhenAll, /, TaskFactory.ContinueWhenAny y los mencionados anteriormente Task.WhenAll, /, Task.WhenAny no. En su lugar, use Task.Delay y Task.WhenAny juntos para implementar un tiempo de espera.
Por ejemplo, en la aplicación de interfaz de usuario, supongamos que desea descargar una imagen y deshabilitar la interfaz de usuario mientras se descarga la imagen. Sin embargo, si la descarga tarda demasiado tiempo, quiere volver a habilitar la interfaz de usuario y descartar la descarga:
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
El mismo principio se aplica a varias descargas, ya que WhenAll devuelve una tarea:
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
Construir combinadores basados en tareas
Dado que una tarea puede representar completamente una operación asincrónica y proporcionar funcionalidades sincrónicas y asincrónicas para unirse a la operación, recuperar sus resultados, etc., puede crear bibliotecas útiles de combinadores que componen tareas para crear patrones más grandes. Como se ha descrito en la sección anterior, .NET incluye varios combinadores integrados, pero también puede crear los suyos propios. En las secciones siguientes se proporcionan varios ejemplos de posibles métodos y tipos de combinadores.
RetryOnFault
En muchas situaciones, querrás volver a intentar una operación si un intento anterior falló. En el caso del código sincrónico, puede crear un método auxiliar como RetryOnFault en el ejemplo siguiente para realizar esta tarea:
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
Puede crear un método auxiliar casi idéntico para las operaciones asincrónicas que se implementan con TAP y, por tanto, devolver tareas:
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
A continuación, puede usar este combinador para codificar los reintentos en la lógica de la aplicación. Por ejemplo:
// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
() => DownloadStringTaskAsync(url), 3);
Puede ampliar aún más la RetryOnFault función. Por ejemplo, la función podría aceptar otra Func<Task> que la función invoca entre reintentos para determinar cuándo intentar la operación de nuevo. Por ejemplo:
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
Después, puede usar la función como se indica a continuación para esperar un segundo antes de volver a intentar la operación:
// 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
A veces, puede aprovechar la redundancia para mejorar la latencia y las posibilidades de éxito de una operación. Tenga en cuenta varios servicios web que proporcionan cotizaciones de acciones, pero en distintos momentos del día, cada servicio podría ofrecer distintos niveles de calidad y tiempo de respuesta. Para tratar estas fluctuaciones, es posible que emita solicitudes a todos los servicios web y, en cuanto obtenga una respuesta de una, cancele las solicitudes restantes. Puede implementar una función auxiliar para facilitar la implementación de este patrón común de inicio de varias operaciones, esperando cualquier y cancelando el resto. La NeedOnlyOne función del ejemplo siguiente ilustra este escenario:
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
A continuación, puede usar esta función de la siguiente manera:
double currentPrice = await NeedOnlyOne(
ct => GetCurrentPriceFromServer1Async("msft", ct),
ct => GetCurrentPriceFromServer2Async("msft", ct),
ct => GetCurrentPriceFromServer3Async("msft", ct));
Operaciones intercaladas
El uso del método WhenAny para admitir un escenario de intercalación puede provocar un problema de rendimiento al trabajar con grandes conjuntos de tareas. Cada llamada a WhenAny registra una continuación con cada tarea. Para N número de tareas, este proceso crea continuaciones de O(N2) durante la vigencia de la operación de intercalación. Si está trabajando con un gran conjunto de tareas, use un combinador (Interleaved en el ejemplo siguiente) para solucionar el problema de rendimiento:
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 el combinador para procesar los resultados de las tareas a medida que se completan. Por ejemplo:
IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
int result = await task;
…
}
WhenAllOrFirstException
En determinados escenarios de dispersión o recopilación, es posible que quiera esperar a que se produzcan todas las tareas de un conjunto, a menos que se produzcan errores en una de ellas. En ese caso, debe dejar de esperar tan pronto como ocurra la excepción. Puede lograr ese comportamiento mediante un método combinador, como WhenAllOrFirstException en el ejemplo siguiente:
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
Creación de estructuras de datos basadas en tareas
Además de la capacidad de crear combinadores personalizados basados en tareas, tener una estructura de datos en Task y Task<TResult> que representa tanto los resultados de una operación asincrónica como la sincronización necesaria para unirse con él hace que sea un tipo eficaz en el que crear estructuras de datos personalizadas que se usarán en escenarios asincrónicos.
AsyncCache
Un aspecto importante de una tarea es que puedes asignarla a varios consumidores. Todos los consumidores pueden esperarlo, registrar continuaciones con él, obtener su resultado o excepciones (en el caso de Task<TResult>), etc. Este aspecto hace que Task y Task<TResult> sean perfectamente adecuados para su uso en una infraestructura de almacenamiento en caché asincrónica. Este es un ejemplo de una caché asincrónica pequeña pero eficaz basada en 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
La clase AsyncCache<TKey,TValue> acepta en su constructor como delegado una función que toma un TKey y devuelve un Task<TResult>. El diccionario interno almacena los valores a los que se ha accedido anteriormente desde la memoria caché y AsyncCache garantiza que genera solo una tarea por clave, incluso si se accede a la memoria caché simultáneamente.
Por ejemplo, puede crear una memoria caché para páginas web descargadas:
private AsyncCache<string,string> m_webPages =
new AsyncCache<string,string>(DownloadStringTaskAsync);
A continuación, puede usar esta memoria caché en métodos asincrónicos siempre que necesite el contenido de una página web. La AsyncCache clase garantiza que está descargando la menor cantidad de páginas posible y almacena en caché los 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
AsyncProducerConsumerCollection
También puede usar tareas para crear estructuras de datos para coordinar actividades asincrónicas. Considere uno de los patrones de diseño paralelo clásicos: productor/consumidor. En este patrón, los productores generan datos que luego consumen los consumidores, y ambos pueden ejecutarse en paralelo. Por ejemplo, el consumidor procesa el elemento 1, que anteriormente generó un productor que ahora está produciendo el artículo 2. Para el patrón de productor o consumidor, siempre necesita alguna estructura de datos para almacenar el trabajo creado por los productores para que los consumidores puedan recibir notificaciones de nuevos datos y encontrarlos cuando estén disponibles.
A continuación se muestra una estructura de datos simple creada sobre las tareas, que permite el uso de métodos asincrónicos como productores y 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
Con esa estructura de datos en su lugar, puede escribir código como el siguiente:
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
El System.Threading.Tasks.Dataflow espacio de nombres incluye el BufferBlock<T> tipo , que puede usar de forma similar, pero sin tener que crear un tipo de colección 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
Nota:
El System.Threading.Tasks.Dataflow espacio de nombres está disponible como un paquete NuGet. Para instalar el ensamblado que contiene el espacio de nombres System.Threading.Tasks.Dataflow, abra su proyecto en Visual Studio, elija Administrar paquetes NuGet en el menú Proyecto y busque en línea el paquete System.Threading.Tasks.Dataflow.