Wann sollte TaskCompletionSource <T> verwendet werden?


199

AFAIK, alles was es weiß ist, dass irgendwann seine SetResultoder SetExceptionMethode aufgerufen wird, um die Task<T>Belichtung durch seine TaskEigenschaft zu vervollständigen .

Mit anderen Worten, es fungiert als Produzent für a Task<TResult>und dessen Fertigstellung.

Ich habe hier das Beispiel gesehen:

Wenn ich eine Möglichkeit brauche, eine Func asynchron auszuführen und eine Aufgabe zu haben, um diese Operation darzustellen.

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

Welche verwendet * werden könnte , wenn ich nicht Task.Factory.StartNew- aber ich tun haben Task.Factory.StartNew.

Frage:

Kann jemand ein Szenario mit gutem Beispiel erklären Sie sich bitte im Zusammenhang direkt an TaskCompletionSource und nicht auf eine hypothetische Situation , in der ich nicht habe Task.Factory.StartNew?


5
TaskCompletionSource wird hauptsächlich zum Umschließen einer ereignisbasierten asynchronen API mit Task verwendet, ohne neue Threads zu erstellen.
Arvis

Antworten:


230

Ich verwende es meistens, wenn nur eine ereignisbasierte API verfügbar ist ( z. B. 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;
}

Daher ist es besonders nützlich, wenn es zusammen mit dem asyncSchlüsselwort C # 5 verwendet wird .


4
Kannst du in Worten schreiben, was wir hier sehen? SomeApiWrapperIst es so, dass irgendwo darauf gewartet wird, bis der Herausgeber das Ereignis auslöst, das dazu führt, dass diese Aufgabe abgeschlossen wird?
Royi Namir

Schauen Sie sich den Link an, den ich gerade hinzugefügt habe
GameScripting

6
Nur ein Update, Microsoft hat das Microsoft.Bcl.AsyncPaket auf NuGet veröffentlicht, das die async/awaitSchlüsselwörter in .NET 4.0-Projekten zulässt (VS2012 und höher wird empfohlen).
Erik

1
@ Fran_gg7 Sie könnten ein CancellationToken verwenden, siehe msdn.microsoft.com/en-us/library/dd997396(v=vs.110).aspx oder als neue Frage hier auf stackoverflow
GameScripting

1
Das Problem mit dieser Implementierung ist, dass dies einen Speicherverlust erzeugt, da das Ereignis nie von obj.Done
Walter Vehoeven

78

Nach meinen Erfahrungen TaskCompletionSourceeignet es sich hervorragend, um alte asynchrone Muster in das moderne async/awaitMuster zu verpacken .

Das nützlichste Beispiel, an das ich denken kann, ist die Arbeit mit Socket. Es hat die alten APM- und EAP-Muster, aber nicht die awaitable TaskMethoden, die TcpListenerund TcpClienthaben.

Ich persönlich habe mehrere Probleme mit der NetworkStreamKlasse und bevorzuge das Rohe Socket. Da ich das async/awaitMuster auch liebe , habe ich eine Erweiterungsklasse SocketExtendererstellt, für die verschiedene Erweiterungsmethoden erstellt werden Socket.

Alle diese Methoden verwenden TaskCompletionSource<T>, um die asynchronen Aufrufe wie folgt zu verpacken:

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

Ich übergebe das socketan die BeginAcceptMethoden, damit der Compiler eine leichte Leistungssteigerung erzielt, ohne den lokalen Parameter hochziehen zu müssen.

Dann die Schönheit von allem:

 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
Warum hätte Task.Factory.StartNew hier nicht funktioniert?
Tola Odejayi

23
@Tola Da dies eine neue Aufgabe erstellt hätte, die auf einem Threadpool-Thread ausgeführt wird, verwendet der obige Code den von BeginAccept gestarteten E / A-Abschluss-Thread, iow: Es wird kein neuer Thread gestartet.
Frans Bouma

4
Danke, @ Frans-Bouma. TaskCompletionSource ist also eine praktische Möglichkeit, Code, der die Anweisungen Begin ... End ... verwendet, in eine Aufgabe zu konvertieren.
Tola Odejayi

3
@TolaOdejayi Eine etwas späte Antwort, aber ja, das ist einer der primären Anwendungsfälle, die ich dafür gefunden habe. Es funktioniert wunderbar für diesen Codeübergang.
Erik

4
Schauen Sie sich die TaskFactory <TResult> .FromAsync an, um Begin.. End...Anweisungen zu verpacken .
MicBig

37

Für mich ist ein klassisches Szenario für die Verwendung, TaskCompletionSourcewenn es möglich ist, dass meine Methode nicht unbedingt einen zeitaufwändigen Vorgang ausführen muss. Damit können wir die spezifischen Fälle auswählen, in denen wir einen neuen Thread verwenden möchten.

Ein gutes Beispiel hierfür ist die Verwendung eines Caches. Sie können eine GetResourceAsyncMethode verwenden, die im Cache nach der angeforderten Ressource sucht und sofort (ohne Verwendung eines neuen Threads mithilfe von TaskCompletionSource) zurückgibt, wenn die Ressource gefunden wurde. Nur wenn die Ressource nicht gefunden wurde, möchten wir einen neuen Thread verwenden und ihn mit abrufen Task.Run().

Ein Codebeispiel finden Sie hier: So führen Sie einen Code mithilfe von Tasks unter bestimmten Bedingungen asynchron aus


Ich habe deine Frage und auch die Antwort gesehen. (siehe meinen Kommentar zur Antwort) .... :-) und in der Tat ist es eine lehrreiche Frage und Antwort.
Royi Namir

11
Dies ist eigentlich keine Situation, in der TCS benötigt wird. Sie können dies einfach verwenden Task.FromResult. Wenn Sie 4.0 verwenden und kein Task.FromResultTCS haben, schreiben Sie natürlich Ihr eigenesFromResult .
Servy

@Servy Task.FromResultist nur seit .NET 4.5 verfügbar. Davor war dies der Weg, um dieses Verhalten zu erreichen.
Adi Lester

@AdiLester Ihre Antwort bezieht Task.Runsich auf 4,5+. Und mein vorheriger Kommentar bezog sich speziell auf .NET 4.0.
Servy

@Servy Nicht jeder, der diese Antwort liest, zielt auf .NET 4.5+ ab. Ich glaube, dies ist eine gute und gültige Antwort, die den Leuten hilft, die Frage des OP zu stellen (die übrigens mit .NET-4.0 gekennzeichnet ist). In jedem Fall scheint mir das Downvoting ein bisschen viel zu sein, aber wenn Sie wirklich glauben, dass es ein Downvote verdient, dann machen Sie weiter.
Adi Lester

25

In diesem Blog-Beitrag beschreibt Levi Botelho, wie Sie mit TaskCompletionSourceas einen asynchronen Wrapper für einen Prozess schreiben, damit Sie ihn starten und auf seine Beendigung warten können.

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

und seine Verwendung

await RunProcessAsync("myexecutable.exe");

14

Es sieht so aus, als hätte niemand etwas erwähnt, aber ich denke, Unit-Tests können auch als real genug angesehen werden.

Ich finde TaskCompletionSourcees nützlich, wenn ich eine Abhängigkeit mit einer asynchronen Methode verspotte.

Im aktuellen Testprogramm:

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

In Unit-Tests:

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

Schließlich scheint diese Verwendung von TaskCompletionSource ein weiterer Fall von "einem Task-Objekt, das keinen Code ausführt" zu sein.


11

TaskCompletionSource wird verwendet, um Task- Objekte zu erstellen , die keinen Code ausführen. In realen Szenarien ist TaskCompletionSource ideal für E / A-gebundene Vorgänge. Auf diese Weise erhalten Sie alle Vorteile von Aufgaben (z. B. Rückgabewerte, Fortsetzungen usw.), ohne einen Thread für die Dauer des Vorgangs zu blockieren. Wenn Ihre "Funktion" eine E / A-gebundene Operation ist, wird nicht empfohlen, einen Thread mit einer neuen Aufgabe zu blockieren . Stattdessen können Sie mit TaskCompletionSource eine Slave-Task erstellen, um nur anzuzeigen, wann Ihre E / A-gebundene Operation beendet ist oder Fehler aufweist.


5

In diesem Beitrag aus dem Blog "Parallele Programmierung mit .NET" gibt es ein Beispiel aus der Praxis mit einer anständigen Erklärung . Sie sollten es wirklich lesen, aber hier ist trotzdem eine Zusammenfassung.

Der Blog-Beitrag zeigt zwei Implementierungen für:

"Eine Factory-Methode zum Erstellen von" verzögerten "Aufgaben, die erst geplant werden, wenn eine vom Benutzer angegebene Zeitüberschreitung aufgetreten ist."

Die erste gezeigte Implementierung basiert auf Task<>und weist zwei Hauptmängel auf. Im zweiten Implementierungsbeitrag werden diese durch die Verwendung gemindert TaskCompletionSource<>.

Hier ist die zweite Implementierung:

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

Es wäre besser, auf tcs.Task zu warten und dann die Aktion () nach
Royi Namir

5
Weil Sie zu dem Kontext zurückkehren, in dem Sie gegangen sind, in dem Continuewith den Kontext nicht bewahrt. (nicht standardmäßig) Auch wenn die nächste Anweisung in action () eine Ausnahme verursacht, ist es schwierig, sie abzufangen, wenn die Verwendung von await Sie als reguläre Ausnahme anzeigt.
Royi Namir

3
Warum nicht einfach await Task.Delay(millisecondsDelay); action(); return;oder (in .Net 4.0)return Task.Delay(millisecondsDelay).ContinueWith( _ => action() );
sgnsajgon

@sgnsajgon, das wäre sicherlich einfacher zu lesen und zu pflegen
JwJosefy

@JwJosefy Tatsächlich kann die Task.Delay- Methode mithilfe von TaskCompletionSource implementiert werden , ähnlich wie im obigen Code. Die eigentliche Implementierung ist hier: Task.cs
sgnsajgon

4

Dies mag die Dinge zu stark vereinfachen, aber die TaskCompletion-Quelle ermöglicht es einem, auf ein Ereignis zu warten. Da das tcs.SetResult erst festgelegt wird, wenn das Ereignis eintritt, kann der Aufrufer auf die Aufgabe warten.

Sehen Sie sich dieses Video an, um weitere Einblicke zu erhalten:

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


1
Bitte geben Sie hier den relevanten Code oder die Dokumentation ein, da sich die Links im Laufe der Zeit ändern und diese Antwort irrelevant machen können.
rfornal

3

Ich habe ein reales Szenario verwendet, in dem ich TaskCompletionSourceeine Download-Warteschlange implementiert habe. In meinem Fall, wenn der Benutzer 100 Downloads startet, möchte ich nicht alle auf einmal auslösen. Anstatt eine geplante Aufgabe zurückzugeben, gebe ich eine angehängte Aufgabe zurück TaskCompletionSource. Sobald der Download abgeschlossen ist, schließt der Thread, der in der Warteschlange arbeitet, die Aufgabe ab.

Das Schlüsselkonzept hier ist, dass ich mich entkopple, wenn ein Client verlangt, dass eine Aufgabe von dem Zeitpunkt an gestartet wird, an dem sie tatsächlich gestartet wird. In diesem Fall, weil ich nicht möchte, dass sich der Client mit dem Ressourcenmanagement befassen muss.

Beachten Sie, dass Sie verwenden können , async / await in .net 4, solange Sie eine C # 5 Compiler (VS 2012+) sehen verwenden hier für weitere Details.


0

Ich habe TaskCompletionSourceeine Aufgabe ausgeführt, bis sie abgebrochen wurde. In diesem Fall handelt es sich um einen ServiceBus-Abonnenten, den ich normalerweise ausführen möchte, solange die Anwendung ausgeführt wird.

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
Es ist nicht erforderlich, 'async' mit 'TaskCompletionSource' zu verwenden, da bereits eine Aufgabe erstellt wurde
Mandeep Janjua

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.