Einfachste Möglichkeit, eine Feuer- und Vergessensmethode in c # 4.0 auszuführen

99

Diese Frage gefällt mir sehr gut:

Einfachste Möglichkeit, eine Feuer- und Vergessensmethode in C # durchzuführen?

Ich möchte nur wissen, dass es jetzt, da wir parallele Erweiterungen in C # 4.0 haben, eine sauberere Möglichkeit gibt, Fire & Forget mit parallel linq auszuführen.

Jonathon Kresner
quelle
1
Die Antwort auf diese Frage gilt weiterhin für .NET 4.0. Feuer und Vergessen wird nicht viel einfacher als QueueUserWorkItem.
Brian Rasmussen

Antworten:

107

Keine Antwort für 4.0, aber erwähnenswert, dass Sie dies in .Net 4.5 noch einfacher machen können mit:

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

Das Pragma besteht darin, die Warnung zu deaktivieren, die besagt, dass Sie diese Aufgabe als Feuer ausführen und vergessen.

Wenn die Methode in geschweiften Klammern eine Aufgabe zurückgibt:

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

Lassen Sie uns das zusammenfassen:

Task.Run gibt eine Task zurück, die eine Compiler-Warnung (Warnung CS4014) generiert, die darauf hinweist, dass dieser Code im Hintergrund ausgeführt wird - genau das wollten Sie, daher deaktivieren wir die Warnung 4014.

Standardmäßig versuchen Aufgaben, "auf den ursprünglichen Thread zurückzugreifen". Dies bedeutet, dass diese Aufgabe im Hintergrund ausgeführt wird, und versuchen dann, zu dem Thread zurückzukehren, der sie gestartet hat. Oft feuern und vergessen Aufgaben werden beendet, nachdem der ursprüngliche Thread fertig ist. Dadurch wird eine ThreadAbortException ausgelöst. In den meisten Fällen ist dies harmlos - es sagt Ihnen nur, ich habe versucht, mich wieder anzuschließen, ich habe versagt, aber es ist Ihnen sowieso egal. Aber es ist immer noch etwas laut, ThreadAbortExceptions entweder in Ihren Protokollen in Production oder in Ihrem Debugger in local dev zu haben. .ConfigureAwait(false)ist nur eine Möglichkeit, ordentlich zu bleiben und explizit zu sagen, dies im Hintergrund auszuführen, und das war's.

Da dies wortreich ist, insbesondere das hässliche Pragma, verwende ich dafür eine Bibliotheksmethode:

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

Verwendung:

TaskHelper.RunBg(async () =>
{
    await doSomethingAsync();
}
Chris Moschini
quelle
7
@ksm Ihr Ansatz ist leider in Schwierigkeiten - haben Sie es getestet? Genau aus diesem Grund gibt es die Warnung 4014. Wenn Sie eine asynchrone Methode ohne Wartezeit und ohne die Hilfe von Task.Run ... aufrufen, wird diese Methode zwar ausgeführt, aber nach Abschluss wird versucht, den ursprünglichen Thread wiederherzustellen, von dem sie ausgelöst wurde. Oft hat dieser Thread die Ausführung bereits abgeschlossen und Ihr Code explodiert auf verwirrende, unbestimmte Weise. Tu es nicht! Der Aufruf von Task.Run ist eine bequeme Möglichkeit, "Führen Sie dies im globalen Kontext aus" zu sagen, und es bleibt nichts übrig, zu dem er versuchen kann, zu marshallen.
Chris Moschini
1
@ ChrisMoschini gerne helfen, und danke für das Update! Auf der anderen Seite, wo ich im obigen Kommentar geschrieben habe "Sie nennen es mit einer asynchronen anonymen Funktion", ist es für mich ehrlich verwirrend, was die Wahrheit ist. Ich weiß nur, dass der Code funktioniert, wenn der Async nicht im aufrufenden Code (anonyme Funktion) enthalten ist. Würde dies jedoch bedeuten, dass der aufrufende Code nicht asynchron ausgeführt wird (was schlecht ist, ein sehr schlechtes Gotcha)? Ich empfehle also nicht ohne Async, es ist nur seltsam, dass in diesem Fall beide funktionieren.
Nicholas Petersen
8
Ich sehe nicht den Punkt von ConfigureAwait(false)auf , Task.Runwenn Sie nicht awaitdie Aufgabe. Der Zweck der Funktion liegt in ihrem Namen: "configure await ". Wenn Sie awaitdie Aufgabe nicht ausführen, registrieren Sie keine Fortsetzung, und es gibt keinen Code für die Aufgabe, wie Sie sagen, "zurück zum ursprünglichen Thread zu marshallen". Das größere Risiko besteht darin, dass eine unbeobachtete Ausnahme im Finalizer-Thread erneut ausgelöst wird, auf die diese Antwort nicht einmal eingeht.
Mike Strobel
3
@stricq Hier gibt es keine asynchronen ungültigen Verwendungen. Wenn Sie sich auf async () => ... beziehen, ist die Signatur eine Funktion, die eine Aufgabe zurückgibt, nicht ungültig.
Chris Moschini
2
Auf VB.net, um Warnung zu unterdrücken:#Disable Warning BC42358
Altiano Gerung
85

Mit dem Task Klasse ja, aber PLINQ ist wirklich zum Abfragen über Sammlungen.

So etwas wie das Folgende wird es mit Task machen.

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

Oder auch...

Task.Factory.StartNew(FireAway);

Oder...

new Task(FireAway).Start();

Wo FireAwayist

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

Aufgrund der Knappheit des Klassen- und Methodennamens übertrifft dies die Threadpool-Version um sechs bis neunzehn Zeichen, je nachdem, welches Sie auswählen :)

ThreadPool.QueueUserWorkItem(o => FireAway());
Ade Miller
quelle
Sicherlich sind sie jedoch nicht gleichwertig mit der Funktionalität?
Jonathon Kresner
6
Es gibt einen subtilen semantischen Unterschied zwischen StartNew und new Task.Start, aber ansonsten ja. Sie alle stellen FireAway in die Warteschlange, um auf einem Thread im Threadpool ausgeführt zu werden.
Ade Miller
Arbeiten für diesen Fall : fire and forget in ASP.NET WebForms and windows.close()?
PreguntonCojoneroCabrón
32

Ich habe ein paar Probleme mit der führenden Antwort auf diese Frage.

Erstens, in einer echten Feuer-und-Vergessen- Situation werden Sie awaitdie Aufgabe wahrscheinlich nicht erledigen, so dass es sinnlos ist, sie anzuhängen ConfigureAwait(false). Wenn Sie nicht awaitden von zurückgegebenen WertConfigureAwait , kann dies möglicherweise keine Auswirkung haben.

Zweitens müssen Sie wissen, was passiert, wenn die Aufgabe mit einer Ausnahme abgeschlossen wird. Betrachten Sie die einfache Lösung, die @ ade-miller vorgeschlagen hat:

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

Dies birgt eine Gefahr: Wenn eine nicht behandelte Ausnahme auftritt SomeMethod(), wird diese Ausnahme niemals beobachtet und möglicherweise 1 im Finalizer-Thread erneut ausgelöst, wodurch Ihre Anwendung abstürzt. Ich würde daher empfehlen, eine Hilfsmethode zu verwenden, um sicherzustellen, dass alle daraus resultierenden Ausnahmen eingehalten werden.

Sie könnten so etwas schreiben:

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

Diese Implementierung sollte einen minimalen Overhead haben: Die Fortsetzung wird nur aufgerufen, wenn die Aufgabe nicht erfolgreich abgeschlossen wurde, und sie sollte synchron aufgerufen werden (anstatt getrennt von der ursprünglichen Aufgabe geplant zu werden). Im "faulen" Fall erhalten Sie nicht einmal eine Zuweisung für den Fortsetzungsdelegierten.

Das Starten einer asynchronen Operation wird dann trivial:

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

1. Dies war das Standardverhalten in .NET 4.0. In .NET 4.5 wurde das Standardverhalten so geändert, dass nicht beobachtete Ausnahmen im Finalizer-Thread nicht erneut ausgelöst werden (obwohl Sie sie möglicherweise weiterhin über das Ereignis UnobservedTaskException in TaskScheduler beobachten). Die Standardkonfiguration kann jedoch überschrieben werden, und selbst wenn Ihre Anwendung .NET 4.5 erfordert, sollten Sie nicht davon ausgehen, dass nicht beobachtete Aufgabenausnahmen harmlos sind.

Mike Strobel
quelle
1
Wenn ein Handler übergeben wird und der ContinueWithaufgrund einer Stornierung aufgerufen wurde, ist die Ausnahmeeigenschaft des Antezedens Null und eine Nullreferenzausnahme wird ausgelöst. Wenn Sie die Option zur Fortsetzung der Aufgabe auf OnlyOnFaultedfestlegen, müssen Sie die Nullprüfung nicht mehr durchführen oder prüfen, ob die Ausnahme null ist, bevor Sie sie verwenden.
Sanmcp
Die Run () -Methode muss für verschiedene Methodensignaturen überschrieben werden. Besser als Erweiterungsmethode von Task.
Stricq
1
@stricq Die Frage zu "Feuer und Vergessen" -Operationen (dh Status nie überprüft und Ergebnis nie beobachtet), also habe ich mich darauf konzentriert. Wenn die Frage ist, wie man sich am saubersten in den Fuß schießt, wird die Frage, welche Lösung aus gestalterischer Sicht "besser" ist, ziemlich umstritten :). Die beste Antwort ist wohl "nicht", aber das hat die Mindestantwortlänge nicht erreicht. Deshalb habe ich mich darauf konzentriert, eine präzise Lösung bereitzustellen, die Probleme vermeidet, die in anderen Antworten enthalten sind. Argumente lassen sich leicht in einen Abschluss einschließen, und ohne Rückgabewert wird die Verwendung Taskzu einem Implementierungsdetail.
Mike Strobel
@stricq Das heißt, ich bin nicht anderer Meinung als Sie. Die Alternative, die Sie vorgeschlagen haben, ist genau das, was ich in der Vergangenheit getan habe, wenn auch aus verschiedenen Gründen. "Ich möchte vermeiden, dass die App abstürzt, wenn ein Entwickler einen Fehler Tasknicht beobachtet " ist nicht dasselbe wie "Ich möchte einen einfachen Weg, um eine Hintergrundoperation zu starten, niemals das Ergebnis zu beobachten, und es ist mir egal, welcher Mechanismus verwendet wird es zu tun." Auf dieser Grundlage stehe ich hinter meiner Antwort auf die gestellte Frage .
Mike Strobel
Arbeiten für diesen Fall: fire and forgetin ASP.NET WebForms und windows.close()?
PreguntonCojoneroCabrón
7

Nur um ein Problem zu beheben, das mit der Antwort von Mike Strobel auftreten wird:

Wenn Sie var task = Task.Run(action)diese Aufgabe verwenden und nachdem Sie ihr eine Fortsetzung zugewiesen haben, besteht die Gefahr, dass TaskSie eine Ausnahme auslösen, bevor Sie der eine Ausnahmebehandlungsfortsetzung zuweisen Task. Die folgende Klasse sollte also frei von diesem Risiko sein:

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

Hier wird das tasknicht direkt ausgeführt, sondern erstellt, eine Fortsetzung zugewiesen, und erst dann wird die Aufgabe ausgeführt, um das Risiko auszuschließen, dass die Aufgabe die Ausführung abschließt (oder eine Ausnahme auslöst), bevor eine Fortsetzung zugewiesen wird.

Die RunMethode hier gibt die Fortsetzung zurück, Tasksodass ich Komponententests schreiben kann, um sicherzustellen, dass die Ausführung abgeschlossen ist. Sie können es jedoch bei Ihrer Verwendung ignorieren.

Mert Akcakaya
quelle
Ist es auch beabsichtigt, dass Sie es nicht zu einer statischen Klasse gemacht haben?
Deantwo
Diese Klasse implementiert die IAsyncManager-Schnittstelle, kann also keine statische Klasse sein.
Mert Akcakaya