Quando utilizzare TaskCompletionSource <T>?


199

AFAIK, tutto ciò che sa è che ad un certo punto, il suo SetResulto SetExceptionmetodo viene chiamato per completare l' Task<T>esposizione attraverso la sua Taskproprietà.

In altre parole, funge da produttore per Task<TResult>ae il suo completamento.

Ho visto qui l'esempio:

Se ho bisogno di un modo per eseguire un Func in modo asincrono e avere un'attività per rappresentare quell'operazione.

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; 
}

Quale potrebbe essere usato * se non avessi Task.Factory.StartNew- Ma ce l' ho Task.Factory.StartNew.

Domanda:

Qualcuno può spiegare con l'esempio uno scenario correlato direttamente alla TaskCompletionSource e non ad una ipotetica situazione in cui non ho Task.Factory.StartNew?


5
TaskCompletionSource viene utilizzato principalmente per il wrapping di API asincrone basate su eventi con Task senza creare nuovi thread.
Arvis,

Risposte:


230

Lo uso principalmente quando è disponibile solo un'API basata su eventi ( ad esempio socket di 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;
}

Quindi è particolarmente utile se usato insieme alla asyncparola chiave C # 5 .


4
puoi scrivere a parole cosa vediamo qui? è così che SomeApiWrapperè atteso da qualche parte, fino a quando l'editore non solleva l'evento che causa il completamento di questo compito?
Royi Namir

dai un'occhiata al link che ho appena aggiunto
GameScripting

6
Solo un aggiornamento, Microsoft ha rilasciato il Microsoft.Bcl.Asyncpacchetto su NuGet che consente le async/awaitparole chiave nei progetti .NET 4.0 (si consiglia VS2012 e versioni successive).
Erik,

1
@ Fran_gg7 potresti usare un CancelToken, vedi msdn.microsoft.com/en-us/library/dd997396(v=vs.110).aspx o come nuova domanda qui su StackOverflow
GameScript

1
Il problema con questa implementazione è che questo genera una perdita di memoria poiché l'evento non viene mai rilasciato da obj.Done
Walter Vehoeven,

78

Nelle mie esperienze, TaskCompletionSourceè ottimo per avvolgere vecchi schemi asincroni con quelli moderni async/await.

L'esempio più utile che mi viene in mente è quando lavoro Socket. Ha la vecchia APM e modelli EAP, ma non i awaitable Taskmetodi che TcpListenere TcpClienthanno.

Personalmente ho diversi problemi con la NetworkStreamclasse e preferisco il raw Socket. Essendo che adoro anche il async/awaitmodello, ho creato una classe di estensione SocketExtenderche crea diversi metodi di estensione per Socket.

Tutti questi metodi utilizzano TaskCompletionSource<T>per avvolgere le chiamate asincrone in questo modo:

    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;
    }

Passo il socketnei BeginAcceptmetodi in modo che ho un leggero incremento delle prestazioni fuori dal compilatore non dover sollevare il parametro locale.

Quindi la bellezza di tutto:

 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();

1
Perché Task.Factory.StartNew non avrebbe funzionato qui?
Tola Odejayi,

23
@Tola Poiché ciò avrebbe creato una nuova attività in esecuzione su un thread threadpool, ma il codice sopra utilizza il thread di completamento I / O avviato da BeginAccept, iow: non avvia un nuovo thread.
Frans Bouma,

4
Grazie, @ Frans-Bouma. Quindi TaskCompletionSource è un modo pratico di convertire il codice che utilizza le istruzioni Begin ... End ... in un'attività?
Tola Odejayi,

3
@TolaOdejayi Un po 'di risposta tardiva, ma sì, questo è uno dei casi d'uso principali che ho trovato per questo. Funziona meravigliosamente per questa transizione di codice.
Erik,

4
Guarda TaskFactory <TResult> .FromAsync per racchiudere le Begin.. End...istruzioni.
MicBig

37

Per me, uno scenario classico per l'utilizzo TaskCompletionSourceè quando è possibile che il mio metodo non debba necessariamente eseguire un'operazione che richiede tempo. Ciò che ci consente di fare è scegliere i casi specifici in cui vorremmo utilizzare un nuovo thread.

Un buon esempio è quando si utilizza una cache. Puoi avere un GetResourceAsyncmetodo, che cerca nella cache la risorsa richiesta e ritorna immediatamente (senza usare un nuovo thread, usando TaskCompletionSource) se la risorsa è stata trovata. Solo se la risorsa non è stata trovata, vorremmo utilizzare un nuovo thread e recuperarlo utilizzando Task.Run().

Un esempio di codice può essere visto qui: Come eseguire in modo condizionale un codice in modo asincrono usando le attività


Ho visto la tua domanda e anche la risposta. (guarda il mio commento alla risposta) .... :-) e in effetti è una domanda e una risposta educativa.
Royi Namir

11
Questa non è in realtà una situazione in cui è necessario TCS. Puoi semplicemente usare Task.FromResultper fare questo. Ovviamente, se stai usando 4.0 e non hai Task.FromResultquello per cui dovresti usare un TCS è scrivere il tuo FromResult .
Servito l'

@Servy Task.FromResultè disponibile solo da .NET 4.5. Prima di quello, quello era il modo per raggiungere questo comportamento.
Adi Lester,

@AdiLester La tua risposta fa riferimento Task.Run, indicando che è 4.5+. E il mio commento precedente riguardava specificamente .NET 4.0.
Servito l'

@Servy Non tutti leggendo questa risposta si rivolgono a .NET 4.5+. Credo che questa sia una risposta valida e valida che aiuta le persone a porre la domanda del PO (che tra l'altro è etichettata .NET-4.0). Ad ogni modo, il downvoting mi sembra un po 'troppo, ma se credi davvero che meriti un downvote, vai avanti.
Adi Lester,

25

In questo post del blog , Levi Botelho descrive come utilizzare il TaskCompletionSourceper scrivere un wrapper asincrono per un processo in modo tale da poterlo avviare e attendere la sua conclusione.

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;
}

e il suo utilizzo

await RunProcessAsync("myexecutable.exe");

14

Sembra che nessuno ne abbia parlato, ma immagino che anche i test unitari possano essere considerati abbastanza reali .

Trovo TaskCompletionSourceutile quando deride una dipendenza con un metodo asincrono.

Nel programma reale in prova:

public interface IEntityFacade
{
  Task<Entity> GetByIdAsync(string id);
}

Nei test unitari:

// 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);
  // ...
}

Dopotutto, questo utilizzo di TaskCompletionSource sembra un altro caso di "un oggetto Task che non esegue il codice".


11

TaskCompletionSource viene utilizzato per creare oggetti Task che non eseguono codice. Negli scenari del mondo reale, TaskCompletionSource è ideale per le operazioni associate I / O. In questo modo, si ottengono tutti i vantaggi delle attività (ad es. Valori restituiti, continuazioni, ecc.) Senza bloccare un thread per la durata dell'operazione. Se la tua "funzione" è un'operazione associata I / O, non è consigliabile bloccare un thread utilizzando una nuova attività . Invece, utilizzando TaskCompletionSource , è possibile creare un'attività slave per indicare solo quando l'operazione di associazione I / O termina o si verificano errori.


5

C'è un esempio reale con una spiegazione decente in questo post dal blog "Parallel Programming with .NET" . Dovresti davvero leggerlo, ma ecco comunque un riassunto.

Il post sul blog mostra due implementazioni per:

"un metodo di fabbrica per la creazione di attività" ritardate ", quelle che non saranno effettivamente programmate fino a quando non si verificherà un timeout fornito dall'utente."

La prima implementazione mostrata si basa su Task<>e presenta due importanti difetti. Il secondo post di implementazione continua a mitigarli usando TaskCompletionSource<>.

Ecco la seconda implementazione:

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();
    });
}

sarebbe meglio usare wait su tcs.Task e poi usare l'azione () dopo
Royi Namir

5
prima di tornare al contesto in cui sei partito, dove Continuewith non conserva il contesto. (non per impostazione predefinita) anche se la prossima istruzione in action () provoca un'eccezione, sarebbe difficile rilevarla laddove l'utilizzo di wait ti mostrerà come un'eccezione regolare.
Royi Namir,

3
Perché non solo await Task.Delay(millisecondsDelay); action(); return;o (in .Net 4.0)return Task.Delay(millisecondsDelay).ContinueWith( _ => action() );
sgnsajgon

@sgnsajgon che sarebbe sicuramente più facile da leggere e da mantenere
JwJosefy,

@JwJosefy In realtà, il metodo Task.Delay può essere implementato usando TaskCompletionSource , in modo simile al codice sopra. La vera implementazione è qui: Task.cs
sgnsajgon

4

Ciò potrebbe semplificare eccessivamente le cose, ma la fonte TaskCompletion consente di attendere un evento. Poiché tcs.SetResult è impostato solo quando si verifica l'evento, il chiamante può attendere l'attività.

Guarda questo video per ulteriori approfondimenti:

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


1
Inserisci qui il codice o la documentazione pertinente poiché i collegamenti possono cambiare nel tempo e rendere irrilevante questa risposta.
rfornal,

3

Lo scenario del mondo reale in cui ho usato TaskCompletionSourceè quando si implementa una coda di download. Nel mio caso, se l'utente avvia 100 download, non voglio licenziarli tutti in una volta e quindi, invece di restituire un'attività strutturata, restituisco un'attività allegata TaskCompletionSource. Una volta completato il download, il thread che funziona la coda completa l'attività.

Il concetto chiave qui è che mi sto disaccoppiando quando un cliente chiede che un'attività venga avviata da quando inizia effettivamente. In questo caso perché non voglio che il cliente debba occuparsi della gestione delle risorse.

nota che puoi usare async / wait in .net 4 fintanto che stai usando un compilatore C # 5 (VS 2012+), vedi qui per maggiori dettagli.


0

Ho usato TaskCompletionSourceper eseguire un'attività fino a quando non viene annullata. In questo caso è un abbonato ServiceBus che normalmente voglio eseguire fino a quando l'applicazione è in esecuzione.

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.
}

1
Non è necessario utilizzare "asincrono" con "TaskCompletionSource" poiché ha già creato un'attività
Mandeep Janjua,

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.