¿Cuándo se debe usar TaskCompletionSource <T>?

199

AFAIK, todo lo que sabe es que en algún momento, es SetResultoSetException método está siendo llamado para completar el Task<T>expuestos a través de su Taskpropiedad.

En otras palabras, actúa como el productor de un Task<TResult> ay su finalización.

Vi aqui el ejemplo:

Si necesito una forma de ejecutar un Func de forma asincrónica 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 tengo Task.Factory.StartNew.

Pregunta:

Por favor alguien puede explicar por ejemplo, un escenario relacionado directamente a TaskCompletionSource y no a una hipotética situación en la que no tengo Task.Factory.StartNew?

Royi Namir
fuente
55
TaskCompletionSource se utiliza principalmente para ajustar la API asíncrona basada en eventos con Task sin crear nuevos subprocesos.
Arvis

Respuestas:

230

Principalmente lo uso cuando solo hay disponible una API basada en eventos ( por ejemplo, sockets de Windows Phone 8 ):

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 asyncpalabra clave C # 5 .

GameScripting
fuente
44
¿Puedes escribir en palabras lo que vemos aquí? ¿Es así como SomeApiWrapperse espera en alguna parte, hasta que el editor presente el evento que hace que esta tarea se complete?
Royi Namir
echa un vistazo al enlace que acabo de agregar
GameScripting
66
Solo una actualización, Microsoft lanzó el Microsoft.Bcl.Asyncpaquete en NuGet que permite las async/awaitpalabras clave en proyectos .NET 4.0 (se recomienda VS2012 y superior).
Erik
1
@ Fran_gg7 podría usar un CancellationToken, consulte msdn.microsoft.com/en-us/library/dd997396(v=vs.110).aspx o como una nueva pregunta aquí en stackoverflow
GameScripting
1
El problema con esta implementación es que esto genera una pérdida de memoria ya que el evento nunca se libera del obj
Walter Vehoeven
78

En mi experiencia, TaskCompletionSourcees ideal para envolver viejos patrones asincrónicos a los modernosasync/await patrón .

El ejemplo más beneficioso que se me ocurre es cuando trabajo Socket. Tiene la edad APM y patrones de EAP, pero no los awaitable Taskmétodos que TcpListenery TcpClienttienen.

Personalmente tengo varios problemas con la NetworkStreamclase y prefiero el crudo Socket. Siendo que también amo el async/awaitpatrón, creé una clase de extensión SocketExtenderque crea varios métodos de extensión Socket.

Todos estos métodos se utilizan TaskCompletionSource<T>para ajustar 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 socketelBeginAccept métodos de modo que consiga un ligero aumento de rendimiento del compilador no tener que levantar 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();
Erik
fuente
1
¿Por qué Task.Factory.StartNew no hubiera funcionado aquí?
Tola Odejayi
23
@Tola Como eso habría creado una nueva tarea que se ejecuta en un subproceso de grupo de subprocesos, pero el código anterior utiliza el subproceso de finalización de E / S iniciado por BeginAccept, iow: no inicia un nuevo subproceso.
Frans Bouma
44
Gracias, @ Frans-Bouma. ¿Entonces TaskCompletionSource es una forma práctica de convertir código que usa las declaraciones Begin ... End ... en una tarea?
Tola Odejayi
3
@TolaOdejayi Una respuesta tardía, pero sí, ese es uno de los principales casos de uso que he encontrado para ello. Funciona maravillosamente para esta transición de código.
Erik
44
Mire el TaskFactory <TResult> .FromAsync para ajustar las Begin.. End...declaraciones.
MicBig
37

Para mí, un escenario clásico para usar TaskCompletionSourcees cuando es posible que mi método no necesariamente tenga que hacer una operación que mucho tiempo. Lo que nos permite hacer es elegir los casos específicos en los que nos gustaría usar un nuevo hilo.

Un buen ejemplo de esto es cuando usa un caché. Puede tener un GetResourceAsyncmétodo, que busca en la memoria caché el recurso solicitado y devuelve de inmediato (sin usar un nuevo hilo, usando 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 un código de forma asincrónica utilizando tareas

Adi Lester
fuente
Vi tu pregunta y también la respuesta. (mire mi comentario a la respuesta) .... :-) y de hecho es una pregunta y respuesta educativa.
Royi Namir
11
En realidad, esta no es una situación en la que se necesita TCS. Simplemente puede usar Task.FromResultpara hacer esto. Por supuesto, si está utilizando 4.0 y no tiene Task.FromResultun TCS para el que usaría, es escribir el suyo FromResult .
Servicio
@Servy Task.FromResultsolo está disponible desde .NET 4.5. Antes de eso, esa era la forma de lograr este comportamiento.
Adi Lester
@AdiLester Su respuesta es hacer referencia Task.Run, lo que indica que es 4.5+. Y mi comentario anterior abordó específicamente .NET 4.0.
Servicio
@Servy No todos los que leen esta respuesta se dirigen a .NET 4.5+. Creo que esta es una respuesta buena y válida que ayuda a las personas a hacer la pregunta del OP (que por cierto está etiquetada como .NET-4.0). De cualquier manera, el voto negativo me parece un poco demasiado, pero si realmente crees que merece un voto negativo, adelante.
Adi Lester
25

En esta publicación de blog , Levi Botelho describe cómo usar el TaskCompletionSourcepara 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");
Sarín
fuente
14

Parece que nadie lo mencionó, pero supongo que las pruebas unitarias también se pueden considerar en la vida real .

Me parece TaskCompletionSourceútil cuando me burlo 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 otro caso de "un objeto Task que no ejecuta código".

superjos
fuente
11

TaskCompletionSource se usa para crear objetos Task que no ejecutan código. En escenarios del mundo real, TaskCompletionSource es ideal para operaciones vinculadas de E / S. De esta manera, obtiene todos los beneficios de las tareas (por ejemplo, valores de retorno, continuaciones, etc.) sin bloquear un hilo durante la operación. Si su "función" es una operación enlazada de E / S, no se recomienda bloquear un hilo usando una nueva Tarea . En cambio, utilizando TaskCompletionSource , puede crear una tarea esclava para indicar cuándo finaliza o falla su operación vinculada de E / S.

v1p3r
fuente
5

Hay un ejemplo del mundo real con una explicación decente en esta publicación del blog "Programación en paralelo con .NET" . Realmente deberías leerlo, pero de todos modos aquí hay un resumen.

La publicación del blog muestra dos implementaciones para:

"un método de fábrica para crear tareas" demoradas ", que no se programarán hasta que se produzca un tiempo de espera proporcionado por el usuario".

La primera implementación mostrada se basa Task<>y tiene dos fallas principales. La segunda publicación de implementación continúa para mitigar estos mediante el uso 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();
    });
}
urig
fuente
sería mejor usar await en tcs.Task y luego usar la acción () después
Royi Namir
55
antes de volver al contexto donde te fuiste, donde Continuewith no preserva el contexto. (no de forma predeterminada) también si la siguiente instrucción en acción () causa una excepción, sería difícil detectarla cuando usar waitit lo muestre como una excepción regular.
Royi Namir
3
¿Por qué no solo await Task.Delay(millisecondsDelay); action(); return;o (en .Net 4.0)return Task.Delay(millisecondsDelay).ContinueWith( _ => action() );
Sgnsajgon
@sgnsajgon que sería sin duda más fácil de leer y mantener
JwJosefy
@JwJosefy En realidad, el método Task.Delay se puede implementar usando TaskCompletionSource , de manera similar al código anterior. La implementación real está aquí: Task.cs
sgnsajgon
4

Esto puede simplificar demasiado las cosas, pero la fuente de TaskCompletion permite esperar en un evento. Como tcs.SetResult solo se establece una vez que se produce el evento, la persona que llama puede esperar la tarea.

Mire este video para obtener más información:

http://channel9.msdn.com/Series/Three-Essential-Tips-for-Async/Lucian03-TipsForAsyncThreadsAndDatabinding

nmishr
fuente
1
Coloque el código o la documentación relevante aquí, ya que los enlaces pueden cambiar con el tiempo y hacer que esta respuesta sea irrelevante.
rfornal
3

El escenario del mundo real en el que he utilizado TaskCompletionSourcees al implementar una cola de descarga. En mi caso, si el usuario inicia 100 descargas, no quiero dispararlas todas a la vez y, en lugar de devolver una tarea estratificada, devuelvo una tarea adjunta TaskCompletionSource. Una vez que se completa la descarga, el hilo que está funcionando la cola completa la tarea.

El concepto clave aquí es que me estoy desacoplando cuando un cliente solicita que se inicie una tarea desde que realmente se inicia. En este caso porque no quiero que el cliente tenga que lidiar con la gestión de recursos.

tenga en cuenta que puede usar async / await en .net 4 siempre que esté usando un compilador C # 5 (VS 2012+), consulte aquí para obtener más detalles.

Yaur
fuente
0

Solía TaskCompletionSourceejecutar una tarea hasta que se cancela. En este caso, es un suscriptor de ServiceBus que normalmente quiero ejecutar mientras se ejecuta la aplicación.

public async Task RunUntilCancellation(
    CancellationToken cancellationToken,
    Func<Task> onCancel)
{
    var doneReceiving = new TaskCompletionSource<bool>();

    cancellationToken.Register(
        async () =>
        {
            await onCancel();
            doneReceiving.SetResult(true); // Signal to quit message listener
        });

    await doneReceiving.Task.ConfigureAwait(false); // Listen until quit signal is received.
}
Johan Gov
fuente
1
No es necesario usar 'async' con 'TaskCompletionSource' ya que ya ha creado una tarea
Mandeep Janjua