Quand utiliser TaskCompletionSource <T>?

199

AFAIK, tout ce qu'il sait, c'est qu'à un moment donné, sa SetResultou SetExceptionméthode est appelée pour compléter l' Task<T>exposition à travers sa Taskpropriété.

En d'autres termes, il agit en tant que producteur pour a Task<TResult>et son achèvement.

J'ai vu ici l'exemple:

Si j'ai besoin d'un moyen d'exécuter un Func de manière asynchrone et d'avoir une tâche pour représenter cette opération.

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

Ce qui pourrait être utilisé * si je n'ai pas Task.Factory.StartNew- mais je n'avoir .Task.Factory.StartNew

Question:

Quelqu'un peut -il expliquer par exemple s'il vous plaît un scénario lié directement à TaskCompletionSource et non à une hypothétique situation dans laquelle je n'ai pas Task.Factory.StartNew?

Royi Namir
la source
5
TaskCompletionSource est principalement utilisé pour encapsuler des API asynchrones basées sur des événements avec Task sans créer de nouveaux threads.
Arvis

Réponses:

230

Je l'utilise principalement lorsque seule une API basée sur les événements est disponible ( par exemple les sockets 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;
}

Il est donc particulièrement utile lorsqu'il est utilisé avec le asyncmot clé C # 5 .

GameScripting
la source
4
pouvez-vous écrire avec des mots que voyons-nous ici? est-ce comme si SomeApiWrapperquelque part était attendu, jusqu'à ce que l'éditeur déclenche l'événement qui a provoqué la fin de cette tâche?
Royi Namir
jetez un œil au lien que je viens d'ajouter
GameScripting
6
Juste une mise à jour, Microsoft a publié le Microsoft.Bcl.Asyncpackage sur NuGet qui autorise les async/awaitmots clés dans les projets .NET 4.0 (VS2012 et supérieur est recommandé).
Erik
1
@ Fran_gg7 vous pouvez utiliser un CancellationToken, voir msdn.microsoft.com/en-us/library/dd997396(v=vs.110).aspx ou comme une nouvelle question ici sur stackoverflow
GameScripting
1
Le problème avec cette implémentation est que cela génère une fuite de mémoire car l'événement n'est jamais libéré de obj.Done
Walter Vehoeven
78

Dans mes expériences, TaskCompletionSourceest idéal pour emballer les anciens modèles asynchrones au async/awaitmodèle moderne .

L'exemple le plus bénéfique auquel je peux penser est lorsque je travaille avec Socket. Il a l'ancienne APM et les modèles EAP, mais pas les awaitable Taskméthodes TcpListeneret TcpClientont.

J'ai personnellement plusieurs problèmes avec la NetworkStreamclasse et je préfère le brut Socket. Étant donné que j'aime aussi le async/awaitmotif, j'ai créé une classe d'extension SocketExtenderqui crée plusieurs méthodes d'extension pour Socket.

Toutes ces méthodes utilisent TaskCompletionSource<T>pour encapsuler les appels asynchrones comme suit:

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

Je passe le socketdans les BeginAcceptméthodes afin que j'obtienne une légère augmentation des performances du compilateur sans avoir à hisser le paramètre local.

Ensuite, la beauté de tout cela:

 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
la source
1
Pourquoi Task.Factory.StartNew n'aurait-il pas fonctionné ici?
Tola Odejayi
23
@Tola Comme cela aurait créé une nouvelle tâche s'exécutant sur un thread threadpool, mais le code ci-dessus utilise le thread de fin d'E / S démarré par BeginAccept, iow: il ne démarre pas un nouveau thread.
Frans Bouma
4
Merci, Frans-Bouma. Donc, TaskCompletionSource est un moyen pratique de convertir du code qui utilise les instructions Begin ... End ... en tâche?
Tola Odejayi
3
@TolaOdejayi Bit d'une réponse tardive, mais oui, c'est l'un des principaux cas d'utilisation que j'ai trouvé pour cela. Cela fonctionne à merveille pour cette transition de code.
Erik
4
Regardez le TaskFactory <TResult> .FromAsync pour encapsuler lesBegin.. End... instructions.
MicBig
37

Pour moi, un scénario classique d'utilisation TaskCompletionSourceest quand il est possible que ma méthode ne doive pas nécessairement faire une opération longue. Ce qu'il nous permet de faire, c'est de choisir les cas spécifiques où nous aimerions utiliser un nouveau fil.

Un bon exemple pour cela est lorsque vous utilisez un cache. Vous pouvez avoir une GetResourceAsyncméthode qui recherche dans le cache la ressource demandée et renvoie immédiatement (sans utiliser de nouveau thread, en utilisant TaskCompletionSource) si la ressource a été trouvée. Seulement si la ressource n'a pas été trouvée, nous aimerions utiliser un nouveau thread et le récupérer en utilisant Task.Run().

Un exemple de code peut être vu ici: Comment exécuter de manière conditionnelle un code de manière asynchrone à l'aide de tâches

Adi Lester
la source
J'ai vu votre question et aussi la réponse. (regardez mon commentaire à la réponse) .... :-) et en effet c'est une question éducative et une réponse.
Royi Namir
11
Ce n'est en fait pas une situation dans laquelle TCS est nécessaire. Vous pouvez simplement utiliser Task.FromResultpour ce faire. Bien sûr, si vous utilisez 4.0 et que vous n'avez pas de Task.FromResultraison d'utiliser un TCS, c'est d' écrire le vôtre FromResult .
Servy
@Servy Task.FromResultn'est disponible que depuis .NET 4.5. Avant cela, c'était le moyen d'atteindre ce comportement.
Adi Lester
@AdiLester Votre réponse fait référence Task.Run, indiquant que c'est 4.5+. Et mon commentaire précédent portait spécifiquement sur .NET 4.0.
Servy
@Servy Tous ceux qui lisent cette réponse ne ciblent pas .NET 4.5+. Je crois que c'est une bonne réponse valable qui aide les gens à poser la question de l'OP (qui est d'ailleurs étiquetée .NET-4.0). Quoi qu'il en soit, le vote en aval me semble un peu trop, mais si vous croyez vraiment qu'il mérite un vote en aval, alors allez-y.
Adi Lester
25

Dans cet article de blog , Levi Botelho décrit comment utiliser le TaskCompletionSourcepour écrire un wrapper asynchrone pour un processus de telle sorte que vous puissiez le lancer et attendre sa fin.

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

et son utilisation

await RunProcessAsync("myexecutable.exe");
Sarin
la source
14

Il semble que personne n'ait mentionné, mais je suppose que les tests unitaires peuvent également être considérés comme assez réels .

Je trouve TaskCompletionSourceutile lorsque l'on se moque d'une dépendance avec une méthode asynchrone.

Dans le programme en cours de test:

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

Dans les tests unitaires:

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

Après tout, cette utilisation de TaskCompletionSource semble être un autre cas «d'un objet Task qui n'exécute pas de code».

superjos
la source
11

TaskCompletionSource est utilisé pour créer des objets Task qui n'exécutent pas de code. Dans les scénarios du monde réel, TaskCompletionSource est idéal pour les opérations liées aux E / S. De cette façon, vous obtenez tous les avantages des tâches (par exemple, les valeurs de retour, les continuations, etc.) sans bloquer un thread pendant la durée de l'opération. Si votre "fonction" est une opération liée aux E / S, il n'est pas recommandé de bloquer un thread à l'aide d'une nouvelle tâche . Au lieu de cela, à l'aide de TaskCompletionSource , vous pouvez créer une tâche esclave pour simplement indiquer quand votre opération liée aux E / S se termine ou si elle ne fonctionne pas.

v1p3r
la source
5

Il y a un exemple du monde réel avec une explication décente dans cet article du blog "Programmation parallèle avec .NET" . Vous devriez vraiment le lire, mais voici quand même un résumé.

Le billet de blog montre deux implémentations pour:

"une méthode d'usine pour créer des tâches" différées ", celles qui ne seront pas réellement planifiées jusqu'à ce qu'un délai d'expiration fourni par l'utilisateur se soit produit."

La première implémentation illustrée est basée sur Task<>et présente deux défauts majeurs. Le deuxième post d'implémentation continue à les atténuer en utilisant TaskCompletionSource<>.

Voici cette deuxième implémentation:

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
la source
serait préférable d'utiliser attendre sur tcs.Task et ensuite utiliser l'action () après
Royi Namir
5
beucase vous êtes de retour au contexte où vous avez quitté, où Continuewith ne préserve pas le contexte. (pas par défaut) également si la prochaine instruction dans action () provoque une exception, il serait difficile de l'attraper où l'utilisation de wait vous montrera une exception régulière.
Royi Namir
3
Pourquoi pas juste await Task.Delay(millisecondsDelay); action(); return;ou (dans .Net 4.0)return Task.Delay(millisecondsDelay).ContinueWith( _ => action() );
sgnsajgon
@sgnsajgon qui serait certainement plus facile à lire et à maintenir
JwJosefy
@JwJosefy En fait, la méthode Task.Delay peut être implémentée en utilisant TaskCompletionSource , de manière similaire au code ci-dessus. La vraie implémentation est ici: Task.cs
sgnsajgon
4

Cela peut simplifier les choses à l'excès, mais la source TaskCompletion permet d'attendre un événement. Étant donné que tcs.SetResult n'est défini que lorsque l'événement se produit, l'appelant peut attendre la tâche.

Regardez cette vidéo pour plus d'informations:

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

nmishr
la source
1
Veuillez placer le code ou la documentation pertinents ici car les liens peuvent changer au fil du temps et rendre cette réponse non pertinente.
rfornal le
3

Je scénario réel où j'ai utilisé TaskCompletionSourceest lors de la mise en œuvre d'une file d'attente de téléchargement. Dans mon cas, si l'utilisateur démarre 100 téléchargements, je ne veux pas tous les déclencher en même temps et donc au lieu de renvoyer une tâche structurée, je renvoie une tâche attachée àTaskCompletionSource . Une fois le téléchargement terminé, le thread qui fonctionne, la file d'attente termine la tâche.

Le concept clé ici est que je fais un découplage lorsqu'un client demande qu'une tâche soit démarrée à partir du moment où elle démarre réellement. Dans ce cas, parce que je ne veux pas que le client ait à gérer la gestion des ressources.

notez que vous pouvez utiliser async / wait dans .net 4 tant que vous utilisez un compilateur C # 5 (VS 2012+) voir ici pour plus de détails.

Yaur
la source
0

J'ai utilisé TaskCompletionSourcepour exécuter une tâche jusqu'à ce qu'elle soit annulée. Dans ce cas, c'est un abonné ServiceBus que je souhaite normalement exécuter aussi longtemps que l'application s'exécute.

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
la source
1
Pas besoin d'utiliser 'async' avec 'TaskCompletionSource' car il a déjà créé une tâche
Mandeep Janjua