110 votes

La façon la plus simple de faire une méthode "fire and forget" en c# 4.0

J'aime beaucoup cette question :

La façon la plus simple de faire une méthode "fire and forget" en C# ?

Je veux juste savoir si, maintenant que nous avons des extensions parallèles en C# 4.0, il existe un moyen plus propre de faire du Fire & Forget avec Parallel linq ?

1 votes

La réponse à cette question s'applique toujours à .NET 4.0. Il n'y a pas plus simple que QueueUserWorkItem.

0 votes

123voto

Chris Moschini Points 7278

Il ne s'agit pas d'une réponse pour la version 4.0, mais il convient de noter qu'en .Net 4.5, vous pouvez simplifier encore plus les choses avec :

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

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

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 ça :

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

Par défaut, les tâches tentent de "revenir sur le fil d'origine", ce qui signifie que la tâche s'exécute en arrière-plan, puis tente de revenir sur le fil qui l'a lancée. Souvent, les Tâches de type "fire and forget" se terminent après que le Thread d'origine ait terminé. Cela entraîne la levée d'une ThreadAbortException. Dans la plupart des cas, cela est inoffensif - il s'agit simplement de vous dire : j'ai essayé de rejoindre, j'ai échoué, mais vous ne vous en souciez pas de toute façon. Mais c'est toujours un peu bruyant d'avoir des 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écuter ceci en arrière-plan, et c'est tout.

Comme c'est un peu long, surtout en ce qui concerne l'horrible pragma, 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);
    }
}

Utilisation :

TaskHelper.RunBg(async () =>
{
    await doSomethingAsync();
}

1 votes

Si MyFireAndForgetMethod est déjà marqué async (auquel cas il devrait être appelé MyFireAndForgetMethodAsync par convention), le code est encore plus simple : #pragma warning disable 4014 MyFireAndForgetMethodAsync() ;

7 votes

@ksm Votre approche va malheureusement avoir des problèmes - 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 await, et sans l'aide de Task.Run... provoquera l'exécution de cette méthode, oui, mais une fois qu'elle sera terminée, elle tentera de revenir au thread d'origine à partir duquel elle a été lancée. Souvent, ce thread aura déjà terminé son exécution et votre code explosera de manière confuse et indéterminée. Ne le faites pas ! L'appel à Task.Run est un moyen commode de dire "Exécutez cette tâche dans le contexte global", ne laissant rien à cette tâche pour qu'elle tente de s'y substituer.

0 votes

Si c'est le cas, pourquoi Task.Run génère-t-il l'avertissement 4014 ?

88voto

Ade Miller Points 7750

Avec le Task oui, mais PLINQ est vraiment pour les requêtes sur les 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();

FireAway es

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

Ainsi, en vertu de la brièveté des noms de classe et de méthode, cette version bat la version threadpool de six à dix-neuf caractères, selon celle que vous choisissez :)

ThreadPool.QueueUserWorkItem(o => FireAway());

0 votes

Il est certain qu'elles ne sont pas équivalentes en termes de fonctionnalité.

7 votes

Il y a 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 dans le pool de threads.

0 votes

Travaillant pour cette affaire : fire and forget in ASP.NET WebForms and windows.close() ?

39voto

Mike Strobel Points 6585

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

D'abord, dans un véritable feu et oublie situation, vous ne pourrez probablement pas await la tâche, il est donc inutile d'ajouter ConfigureAwait(false) . Si vous ne await la valeur renvoyée par ConfigureAwait alors il ne peut pas avoir d'effet.

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

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

Cela introduit un risque : si une exception non gérée s'échappe de l'application SomeMethod() cette exception ne sera jamais respectée, et pourra 1 sera relancé sur le thread du finalisateur, ce qui fera planter votre application. Je recommande donc d'utiliser une méthode d'aide pour s'assurer que toutes les exceptions qui en résultent sont respectées.

Vous pourriez écrire quelque chose comme ça :

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 mise en œuvre devrait avoir une surcharge minimale : la continuation n'est invoquée que si la tâche ne se termine pas avec succès, et elle devrait être invoquée de manière synchrone (par opposition à une planification séparée de la tâche originale). Dans le cas "paresseux", vous n'aurez même pas besoin d'allocation pour le délégué de continuation.

Le lancement d'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 manière à ce que les exceptions non observées soient <em>no </em>sont rejetées sur le thread du finalisateur (bien que vous puissiez toujours les observer via l'événement UnobservedTaskException sur TaskScheduler). Cependant, la configuration par défaut peut être modifié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.

9voto

Mert Points 1185

Juste pour régler un problème qui va se produire avec la réponse de Mike Strobel :

Si vous utilisez var task = Task.Run(action) et après avoir assigné une continuation à cette tâche, alors vous courrez un risque de Task en lançant une exception avant d'assigner une continuation du gestionnaire d'exception à la fonction Task . La classe ci-dessous devrait donc ê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 task n'est pas exécutée directement, elle est créée, une continuation est assignée, et seulement ensuite 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'assigner une continuation.

El Run retourne ici la suite Task afin que je puisse écrire des tests unitaires en m'assurant que l'exécution est complète. Vous pouvez cependant l'ignorer dans votre utilisation.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X