Méthode la plus simple pour faire un feu et oublier la méthode en c # 4.0

100

J'aime vraiment cette question:

Le moyen le plus simple de faire un feu et d'oublier la méthode en C #?

Je veux juste savoir que maintenant que nous avons des extensions Parallel en C # 4.0, y a-t-il une meilleure façon plus propre de faire Fire & Forget avec Parallel linq?

Jonathon Kresner
la source
1
La réponse à cette question s'applique toujours à .NET 4.0. Fire and forget n'est pas beaucoup plus simple que QueueUserWorkItem.
Brian Rasmussen

Réponses:

108

Pas une réponse pour 4.0, mais il convient de noter que dans .Net 4.5, vous pouvez rendre cela encore plus simple avec:

#pragma warning disable 4014
Task.Run(() =>
{
    MyFireAndForgetMethod();
}).ConfigureAwait(false);
#pragma warning restore 4014

Le pragma est de désactiver l'avertissement qui vous indique que vous exécutez cette tâche en tant que feu et oubliez.

Si la méthode à l'intérieur des accolades renvoie une tâche:

#pragma warning disable 4014
Task.Run(async () =>
{
    await MyFireAndForgetMethod();
}).ConfigureAwait(false);
#pragma warning restore 4014

Décomposons cela:

Task.Run renvoie une tâche, qui génère un avertissement du compilateur (avertissement CS4014) indiquant que ce code sera exécuté en arrière-plan - c'est exactement ce que vous vouliez, nous désactivons donc l'avertissement 4014.

Par défaut, les tâches tentent de «rétablir le thread d'origine», ce qui signifie que cette tâche s'exécutera en arrière-plan, puis tentera de revenir au thread qui l'a démarré. Souvent, déclenchez et oubliez que les tâches se terminent une fois le thread d'origine terminé. Cela entraînera la levée d'une ThreadAbortException. Dans la plupart des cas, c'est inoffensif - c'est juste vous dire, j'ai essayé de rejoindre, j'ai échoué, mais vous vous en fichez de toute façon. Mais il est toujours un peu bruyant d'avoir ThreadAbortExceptions soit dans vos journaux en production, soit dans votre débogueur en développement local. .ConfigureAwait(false)est juste un moyen de rester ordonné et de dire explicitement, exécutez ceci en arrière-plan, et c'est tout.

Comme c'est verbeux, en particulier le pragma laid, j'utilise une méthode de bibliothèque pour cela:

public static class TaskHelper
{
    /// <summary>
    /// Runs a TPL Task fire-and-forget style, the right way - in the
    /// background, separate from the current thread, with no risk
    /// of it trying to rejoin the current thread.
    /// </summary>
    public static void RunBg(Func<Task> fn)
    {
        Task.Run(fn).ConfigureAwait(false);
    }

    /// <summary>
    /// Runs a task fire-and-forget style and notifies the TPL that this
    /// will not need a Thread to resume on for a long time, or that there
    /// are multiple gaps in thread use that may be long.
    /// Use for example when talking to a slow webservice.
    /// </summary>
    public static void RunBgLong(Func<Task> fn)
    {
        Task.Factory.StartNew(fn, TaskCreationOptions.LongRunning)
            .ConfigureAwait(false);
    }
}

Usage:

TaskHelper.RunBg(async () =>
{
    await doSomethingAsync();
}
Chris Moschini
la source
7
@ksm Votre approche est malheureusement en difficulté - l'avez-vous testée? Cette approche est exactement la raison pour laquelle l'avertissement 4014 existe. L'appel d'une méthode asynchrone sans attendre et sans l'aide de Task.Run ... entraînera l'exécution de cette méthode, oui, mais une fois terminée, elle tentera de revenir au thread d'origine à partir duquel elle a été déclenchée. Souvent, ce thread aura déjà terminé son exécution et votre code explosera de manière déroutante et indéterminée. Ne fais pas ça! L'appel à Task.Run est un moyen pratique de dire «Exécutez ceci dans le contexte global», ne laissant rien à lui pour tenter de rassembler.
Chris Moschini
1
@ChrisMoschini heureux de vous aider et merci pour la mise à jour! Sur l'autre question, où j'ai écrit "vous l'appelez avec une fonction anonyme asynchrone" dans le commentaire ci-dessus, c'est honnêtement déroutant pour moi qui est la vérité. Tout ce que je sais, c'est que le code fonctionne lorsque l'async n'est pas inclus dans le code appelant (fonction anonyme), mais cela signifierait-il que le code appelant ne serait pas exécuté de manière asynchrone (ce qui est mauvais, un très mauvais gotcha)? Donc, je ne recommande pas sans l'async, c'est juste étrange que dans ce cas, les deux fonctionnent.
Nicholas Petersen
8
Je ne vois pas l'intérêt de ConfigureAwait(false)sur Task.Runquand vous ne faites pas awaitla tâche. Le but de la fonction est dans son nom: "configure wait ". Si vous n'effectuez pas awaitla tâche, vous n'enregistrez pas de continuation et il n'y a pas de code pour que la tâche «rassemble en arrière sur le thread d'origine», comme vous dites. Le plus grand risque est qu'une exception non observée soit renvoyée sur le thread du finaliseur, que cette réponse ne traite même pas.
Mike Strobel
3
@stricq Il n'y a aucune utilisation async void ici. Si vous faites référence à async () => ... la signature est un Func qui renvoie une Task, pas void.
Chris Moschini
2
Sur VB.net pour supprimer l'avertissement:#Disable Warning BC42358
Altiano Gerung
85

Avec la Taskclasse oui, mais PLINQ est vraiment pour interroger des collections.

Quelque chose comme ce qui suit le fera avec Task.

Task.Factory.StartNew(() => FireAway());

Ou même...

Task.Factory.StartNew(FireAway);

Ou...

new Task(FireAway).Start();

FireAwayest

public static void FireAway()
{
    // Blah...
}

Donc, en raison de la concordance des noms de classe et de méthode, cela bat la version de threadpool de six à dix-neuf caractères selon celui que vous choisissez :)

ThreadPool.QueueUserWorkItem(o => FireAway());
Ade Miller
la source
Mais ils ne sont certainement pas des fonctionnalités équivalentes?
Jonathon Kresner
6
Il existe une différence sémantique subtile entre StartNew et new Task.Start mais sinon, oui. Ils mettent tous FireAway en file d'attente pour qu'il s'exécute sur un thread du pool de threads.
Ade Miller
Travailler pour ce cas fire and forget in ASP.NET WebForms and windows.close():?
PreguntonCojoneroCabrón
32

J'ai quelques problèmes avec la réponse principale à cette question.

Premièrement, dans une véritable situation d' incendie et d'oubli , vous n'aurez probablement pas awaitla tâche, il est donc inutile d'ajouter ConfigureAwait(false). Si vous n'indiquez pas awaitla valeur renvoyée par ConfigureAwait, cela ne peut avoir aucun effet.

Deuxièmement, vous devez être conscient de ce qui se passe lorsque la tâche se termine avec une exception. Considérez la solution simple suggérée par @ ade-miller:

Task.Factory.StartNew(SomeMethod);  // .NET 4.0
Task.Run(SomeMethod);               // .NET 4.5

Cela introduit un danger: si une exception non gérée s'échappe SomeMethod(), cette exception ne sera jamais, et elle pourrait 1 soit relancée sur le thread finaliseur, plantage de votre application. Je recommanderais donc d'utiliser une méthode d'assistance pour s'assurer que toutes les exceptions qui en résultent sont respectées.

Vous pourriez écrire quelque chose comme ceci:

public static class Blindly
{
    private static readonly Action<Task> DefaultErrorContinuation =
        t =>
        {
            try { t.Wait(); }
            catch {}
        };

    public static void Run(Action action, Action<Exception> handler = null)
    {
        if (action == null)
            throw new ArgumentNullException(nameof(action));

        var task = Task.Run(action);  // Adapt as necessary for .NET 4.0.

        if (handler == null)
        {
            task.ContinueWith(
                DefaultErrorContinuation,
                TaskContinuationOptions.ExecuteSynchronously |
                TaskContinuationOptions.OnlyOnFaulted);
        }
        else
        {
            task.ContinueWith(
                t => handler(t.Exception.GetBaseException()),
                TaskContinuationOptions.ExecuteSynchronously |
                TaskContinuationOptions.OnlyOnFaulted);
        }
    }
}

Cette implémentation doit avoir une surcharge minimale: la continuation n'est invoquée que si la tâche ne se termine pas correctement, et elle doit être appelée de manière synchrone (au lieu d'être planifiée séparément de la tâche d'origine). Dans le cas "paresseux", vous n'encourrez même pas une allocation pour le délégué de continuation.

Lancer une opération asynchrone devient alors trivial:

Blindly.Run(SomeMethod);                              // Ignore error
Blindly.Run(SomeMethod, e => Log.Warn("Whoops", e));  // Log error

1. C'était le comportement par défaut dans .NET 4.0. Dans .NET 4.5, le comportement par défaut a été modifié de telle sorte que les exceptions non observées ne seraient pas renvoyées sur le thread du finaliseur (bien que vous puissiez toujours les observer via l'événement UnobservedTaskException sur TaskScheduler). Cependant, la configuration par défaut peut être remplacée, et même si votre application nécessite .NET 4.5, vous ne devez pas supposer que les exceptions de tâches non observées seront inoffensives.

Mike Strobel
la source
1
Si un gestionnaire est passé et que le a ContinueWithété appelé en raison d'une annulation, la propriété d'exception de l'antécédent sera Null et une exception de référence nulle sera levée. La définition de l'option de continuation de la tâche sur OnlyOnFaultedsupprimera la nécessité de vérifier la valeur nulle ou de vérifier si l'exception est nulle avant de l'utiliser.
sanmcp
La méthode Run () doit être remplacée pour différentes signatures de méthode. Mieux vaut faire cela comme une méthode d'extension de Task.
stricq
1
@stricq La question posée sur les opérations "fire and forget" (ie, statut jamais vérifié et résultat jamais observé), c'est donc ce sur quoi je me suis concentré. Quand la question est de savoir comment se tirer une balle dans le pied le plus proprement, débattre de la solution «meilleure» du point de vue du design devient plutôt théorique :). La meilleure réponse est sans doute «ne pas», mais cela ne correspondait pas à la longueur de réponse minimale, donc je me suis concentré sur la fourniture d'une solution concise qui évite les problèmes présents dans d'autres réponses. Les arguments sont facilement encapsulés dans une fermeture, et sans valeur de retour, l'utilisation Taskdevient un détail d'implémentation.
Mike Strobel
@stricq Cela dit, je ne suis pas en désaccord avec vous. L'alternative que vous avez proposée est exactement ce que j'ai fait dans le passé, mais pour des raisons différentes. "Je veux éviter de planter l'application lorsqu'un développeur ne parvient pas à observer une erreur Task" n'est pas la même chose que "Je veux un moyen simple de lancer une opération en arrière-plan, ne jamais observer son résultat, et je me fiche du mécanisme utilisé pour le faire." Sur cette base, je soutiens ma réponse à la question qui a été posée .
Mike Strobel
Travailler pour ce cas: fire and forgetdans ASP.NET WebForms et windows.close()?
PreguntonCojoneroCabrón
7

Juste pour résoudre un problème qui se produira avec la réponse de Mike Strobel:

Si vous utilisez var task = Task.Run(action)et après attribuez une continuation à cette tâche, vous courrez le risque de Tasklever une exception avant d'affecter une continuation de gestionnaire d'exceptions au Task. Ainsi, la classe ci-dessous devrait être exempte de ce risque:

using System;
using System.Threading.Tasks;

namespace MyNameSpace
{
    public sealed class AsyncManager : IAsyncManager
    {
        private Action<Task> DefaultExeptionHandler = t =>
        {
            try { t.Wait(); }
            catch { /* Swallow the exception */ }
        };

        public Task Run(Action action, Action<Exception> exceptionHandler = null)
        {
            if (action == null) { throw new ArgumentNullException(nameof(action)); }

            var task = new Task(action);

            Action<Task> handler = exceptionHandler != null ?
                new Action<Task>(t => exceptionHandler(t.Exception.GetBaseException())) :
                DefaultExeptionHandler;

            var continuation = task.ContinueWith(handler,
                TaskContinuationOptions.ExecuteSynchronously
                | TaskContinuationOptions.OnlyOnFaulted);
            task.Start();

            return continuation;
        }
    }
}

Ici, le taskn'est pas exécuté directement, au lieu de cela, il est créé, une continuation est affectée, et alors seulement la tâche est exécutée pour éliminer le risque que la tâche termine l'exécution (ou lève une exception) avant d'attribuer une continuation.

La Runméthode ici renvoie la suite Taskpour que je puisse écrire des tests unitaires en m'assurant que l'exécution est terminée. Vous pouvez cependant l'ignorer en toute sécurité dans votre utilisation.

Mert Akcakaya
la source
Est-ce également intentionnel que vous n'en ayez pas fait une classe statique?
Deantwo
Cette classe implémente l'interface IAsyncManager, elle ne peut donc pas être une classe statique.
Mert Akcakaya