c# - cref - ¿Cuándo se debe usar TaskCompletionSource<T>?
remarks c# (9)
AFAIK, todo lo que sabe es que en algún punto, se está SetResult
su método SetResult
o SetException
para completar la Task<T>
expuesta a través de su propiedad Task
.
En otras palabras, actúa como el productor de una Task<TResult>
y su finalización.
Vi here el ejemplo:
Si necesito una forma de ejecutar un Func de forma asíncrona y tengo una tarea para representar esa operación.
public static Task<T> RunAsync<T>(Func<T> function)
{
if (function == null) throw new ArgumentNullException(“function”);
var tcs = new TaskCompletionSource<T>();
ThreadPool.QueueUserWorkItem(_ =>
{
try
{
T result = function();
tcs.SetResult(result);
}
catch(Exception exc) { tcs.SetException(exc); }
});
return tcs.Task;
}
Que podría usarse * si no tuviera Task.Factory.StartNew
- Pero sí tengo Task.Factory.StartNew
.
Pregunta:
¿Puede alguien explicar por ejemplo un escenario relacionado directamente con TaskCompletionSource
y no con una situación hipotética en la que no tengo Task.Factory.StartNew
?
El escenario del mundo real donde utilicé TaskCompletionSource
es al implementar una cola de descarga. En mi caso, si el usuario inicia 100 descargas, no quiero despedirlas todas al mismo tiempo, así que en lugar de devolver una tarea TaskCompletionSource
devuelvo una tarea adjunta a TaskCompletionSource
. Una vez que se completa la descarga, el hilo que está trabajando en la cola completa la tarea.
El concepto clave aquí es que estoy desacoplando cuando un cliente solicita que se inicie una tarea desde el momento en que realmente comienza. En este caso, porque no quiero que el cliente tenga que lidiar con la administración de recursos.
tenga en cuenta que puede usar async / await en .net 4 siempre que esté usando un compilador C # 5 (VS 2012+) vea here para más detalles.
En esta publicación de blog , Levi Botelho describe cómo usar TaskCompletionSource
para escribir un contenedor asíncrono para un Proceso de modo que pueda iniciarlo y esperar su finalización.
public static Task RunProcessAsync(string processPath)
{
var tcs = new TaskCompletionSource<object>();
var process = new Process
{
EnableRaisingEvents = true,
StartInfo = new ProcessStartInfo(processPath)
{
RedirectStandardError = true,
UseShellExecute = false
}
};
process.Exited += (sender, args) =>
{
if (process.ExitCode != 0)
{
var errorMessage = process.StandardError.ReadToEnd();
tcs.SetException(new InvalidOperationException("The process did not exit correctly. " +
"The corresponding error message was: " + errorMessage));
}
else
{
tcs.SetResult(null);
}
process.Dispose();
};
process.Start();
return tcs.Task;
}
y su uso
await RunProcessAsync("myexecutable.exe");
En mi experiencia, TaskCompletionSource
es ideal para envolver los viejos patrones asíncronos al moderno patrón async/await
.
El ejemplo más beneficioso que puedo pensar es cuando trabajo con Socket
. Tiene los viejos patrones APM y EAP, pero no los métodos de awaitable Task
TcpListener
que tienen TcpListener
y TcpClient
.
Personalmente tengo varios problemas con la clase NetworkStream
y prefiero el Socket
bruto. Siendo que también amo el patrón async/await
SocketExtender
, hice una clase de extensión SocketExtender
que crea varios métodos de extensión para Socket
.
Todos estos métodos utilizan TaskCompletionSource<T>
para envolver las llamadas asincrónicas de la siguiente manera:
public static Task<Socket> AcceptAsync(this Socket socket)
{
if (socket == null)
throw new ArgumentNullException("socket");
var tcs = new TaskCompletionSource<Socket>();
socket.BeginAccept(asyncResult =>
{
try
{
var s = asyncResult.AsyncState as Socket;
var client = s.EndAccept(asyncResult);
tcs.SetResult(client);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
}, socket);
return tcs.Task;
}
Paso el socket
en los métodos de BeginAccept
para que obtenga un leve aumento de rendimiento fuera del compilador sin tener que izar el parámetro local.
Entonces la belleza de todo:
var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
listener.Bind(new IPEndPoint(IPAddress.Loopback, 2610));
listener.Listen(10);
var client = await listener.AcceptAsync();
Esto puede simplificar demasiado las cosas, pero la fuente TaskCompletion le permite esperar en un evento. Dado que tcs.SetResult solo se establece una vez que ocurre el evento, la persona que llama puede esperar en la tarea.
Mire este video para obtener más información:
Hay un ejemplo del mundo real con una explicación decente en este post del blog "Programación en paralelo con .NET" . Deberías leerlo, pero aquí hay un resumen de todos modos.
La publicación del blog muestra dos implementaciones para:
"un método de fábrica para crear tareas" demoradas ", que en realidad no se programarán hasta que se haya producido un tiempo de espera proporcionado por el usuario".
La primera implementación que se muestra se basa en la Task<>
y tiene dos defectos principales. La segunda publicación de implementación continúa mitigando estos mediante el uso de TaskCompletionSource<>
.
Aquí está esa segunda implementación:
public static Task StartNewDelayed(int millisecondsDelay, Action action)
{
// Validate arguments
if (millisecondsDelay < 0)
throw new ArgumentOutOfRangeException("millisecondsDelay");
if (action == null) throw new ArgumentNullException("action");
// Create a trigger used to start the task
var tcs = new TaskCompletionSource<object>();
// Start a timer that will trigger it
var timer = new Timer(
_ => tcs.SetResult(null), null, millisecondsDelay, Timeout.Infinite);
// Create and return a task that will be scheduled when the trigger fires.
return tcs.Task.ContinueWith(_ =>
{
timer.Dispose();
action();
});
}
La uso principalmente cuando solo está disponible una API basada en eventos ( por ejemplo, Windows Phone 8 Sockets ):
public Task<Args> SomeApiWrapper()
{
TaskCompletionSource<Args> tcs = new TaskCompletionSource<Args>();
var obj = new SomeApi();
// will get raised, when the work is done
obj.Done += (args) =>
{
// this will notify the caller
// of the SomeApiWrapper that
// the task just completed
tcs.SetResult(args);
}
// start the work
obj.Do();
return tcs.Task;
}
Por lo tanto, es especialmente útil cuando se usa junto con la palabra clave async
c # 5.
Para mí, un escenario clásico para usar TaskCompletionSource
es cuando es posible que mi método no necesariamente tenga que realizar una operación que requiera mucho tiempo. Lo que nos permite hacer es elegir los casos específicos en los que nos gustaría utilizar un nuevo hilo.
Un buen ejemplo de esto es cuando usas un caché. Puede tener un método GetResourceAsync
, que busca en el caché el recurso solicitado y lo devuelve a la vez (sin utilizar un nuevo hilo, utilizando TaskCompletionSource
) si se encontró el recurso. Solo si no se encontró el recurso, nos gustaría usar un nuevo hilo y recuperarlo usando Task.Run()
.
Aquí se puede ver un ejemplo de código: cómo ejecutar de forma condicional un código de manera asincrónica utilizando tareas
Parece que nadie mencionó, pero creo que las pruebas unitarias también pueden considerarse vida real .
Encuentro TaskCompletionSource
para ser útil al burlarse de una dependencia con un método asíncrono.
En el programa real bajo prueba:
public interface IEntityFacade
{
Task<Entity> GetByIdAsync(string id);
}
En pruebas unitarias:
// set up mock dependency (here with NSubstitute)
TaskCompletionSource<Entity> queryTaskDriver = new TaskCompletionSource<Entity>();
IEntityFacade entityFacade = Substitute.For<IEntityFacade>();
entityFacade.GetByIdAsync(Arg.Any<string>()).Returns(queryTaskDriver.Task);
// later on, in the "Act" phase
private void When_Task_Completes_Successfully()
{
queryTaskDriver.SetResult(someExpectedEntity);
// ...
}
private void When_Task_Gives_Error()
{
queryTaskDriver.SetException(someExpectedException);
// ...
}
Después de todo, este uso de TaskCompletionSource parece ser otro caso de "un objeto Task que no ejecuta código".
TaskCompletionSource se usa para crear objetos Task que no ejecutan código. En escenarios del mundo real TaskCompletionSource es ideal para operaciones de E / S vinculadas. De esta forma, obtiene todos los beneficios de las tareas (por ejemplo, valores de retorno, continuaciones, etc.) sin bloquear un hilo mientras dura la operación. Si su "función" es una operación enlazada IO, no se recomienda bloquear un hilo utilizando una nueva Tarea . En lugar de usar TaskCompletionSource , puede crear una tarea esclava para indicar simplemente cuándo finaliza o falla su operación de E / S ligada.