Najprostszy sposób na wykonanie polecenia odpal i zapomnij w języku C # 4.0

106

Naprawdę podoba mi się to pytanie:

Najprostszy sposób na wykonanie metody „odpal i zapomnij” w języku C #?

Chcę tylko wiedzieć, że teraz, gdy mamy rozszerzenia równoległe w C # 4.0, czy istnieje lepszy, czystszy sposób na wykonanie polecenia Fire & Forget z Parallel linq?

Jonathon Kresner
źródło
1
Odpowiedź na to pytanie nadal dotyczy .NET 4.0. Uruchom i zapomnij nie jest dużo prostsze niż QueueUserWorkItem.
Brian Rasmussen

Odpowiedzi:

116

Nie jest to odpowiedź na 4.0, ale warto zauważyć, że w .Net 4.5 możesz to jeszcze uprościć dzięki:

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

Pragmą jest wyłączenie ostrzeżenia, które mówi, że uruchamiasz to zadanie jako uruchom i zapomnij.

Jeśli metoda wewnątrz nawiasów klamrowych zwraca Task:

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

Rozbijmy to:

Task.Run zwraca Task, który generuje ostrzeżenie kompilatora (ostrzeżenie CS4014) informujące, że ten kod będzie uruchamiany w tle - dokładnie tego chciałeś, więc wyłączamy ostrzeżenie 4014.

Domyślnie zadania podejmują próbę „Marshal z powrotem do oryginalnego wątku”, co oznacza, że ​​to zadanie będzie działać w tle, a następnie spróbuje powrócić do wątku, który je uruchomił. Często uruchamiaj i zapominaj o zakończeniu zadań po zakończeniu oryginalnego wątku. Spowoduje to zgłoszenie wyjątku ThreadAbortException. W większości przypadków jest to nieszkodliwe - po prostu ci mówi, że próbowałem dołączyć, zawiodłem, ale i tak cię to nie obchodzi. Ale nadal jest trochę głośno, aby mieć ThreadAbortExceptions w dziennikach w produkcji lub w debugerze w lokalnym programie deweloperskim. .ConfigureAwait(false)to tylko sposób na zachowanie porządku i jednoznaczne stwierdzenie, że uruchom to w tle i to wszystko.

Ponieważ jest to rozwlekłe, zwłaszcza brzydka pragma, używam do tego metody bibliotecznej:

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

Stosowanie:

TaskHelper.RunBg(async () =>
{
    await doSomethingAsync();
}
Chris Moschini
źródło
7
@ksm Twoje podejście jest niestety kłopotliwe - czy przetestowałeś je? Właśnie takie podejście jest powodem, dla którego istnieje Ostrzeżenie 4014. Wywołanie metody asynchronicznej bez czekania i bez pomocy Task.Run ... spowoduje uruchomienie tej metody, tak, ale po jej zakończeniu spróbuje wrócić do oryginalnego wątku, z którego została uruchomiona. Często ten wątek już zakończył wykonywanie, a twój kod eksploduje na mylące, nieokreślone sposoby. Nie rób tego! Wywołanie Task.Run to wygodny sposób powiedzenia „Uruchom to w kontekście globalnym”, nie pozostawiając nic, do czego mógłby się kierować.
Chris Moschini
1
@ChrisMoschini cieszę się, że mogę pomóc i dzięki za aktualizację! Z drugiej strony, gdy w powyższym komentarzu napisałem „nazywasz to asynchroniczną funkcją anonimową”, to jest dla mnie mylące, co jest prawdą. Wiem tylko, że kod działa, gdy asynchroniczny kod nie jest zawarty w kodzie wywołującym (funkcji anonimowej), ale czy oznaczałoby to, że kod wywołujący nie byłby uruchamiany w sposób asynchroniczny (co jest złe, bardzo zły problem)? Więc nie polecam bez async, to po prostu dziwne, że w tym przypadku oba działają.
Nicholas Petersen
8
Nie widzę sensu ConfigureAwait(false)w Task.Runjeśli nie zrobić awaitzadanie. Celem funkcji jest w jej nazwie: „configure await ”. Jeśli nie wykonasz awaitzadania, nie zarejestrujesz kontynuacji i nie ma kodu, który pozwoliłby, jak to powiedzieć, „przenieść z powrotem do oryginalnego wątku”. Większe ryzyko wiąże się z ponownym zgłoszeniem niezauważonego wyjątku w wątku finalizatora, którego ta odpowiedź nawet nie dotyczy.
Mike Strobel
3
@stricq Nie ma tutaj asynchronicznych pustych zastosowań. Jeśli odnosisz się do async () => ... podpis jest Func, który zwraca Task, a nie void.
Chris Moschini
2
Na VB.net, aby wyłączyć ostrzeżenie:#Disable Warning BC42358
Altiano Gerung,
86

Z Taskklasą tak, ale PLINQ jest tak naprawdę do przeszukiwania kolekcji.

Coś takiego jak poniższe zrobi to z Task.

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

Lub nawet...

Task.Factory.StartNew(FireAway);

Lub...

new Task(FireAway).Start();

Gdzie FireAwayjest

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

Tak więc dzięki zwięzłości nazwy klasy i metody przewyższa wersję puli wątków o od sześciu do dziewiętnastu znaków w zależności od tego, który wybierzesz :)

ThreadPool.QueueUserWorkItem(o => FireAway());
Ade Miller
źródło
Z pewnością jednak nie są one równoważne funkcjonalnie?
Jonathon Kresner
6
Istnieje subtelna semantyczna różnica między StartNew i new Task.Start, ale poza tym tak. Wszystkie ustawiają FireAway w kolejce do uruchomienia w wątku w puli wątków.
Ade Miller,
Praca w tej sprawie fire and forget in ASP.NET WebForms and windows.close():?
PreguntonCojoneroCabrón
34

Mam kilka problemów z wiodącą odpowiedzią na to pytanie.

Po pierwsze, w prawdziwej sytuacji „ odpal i zapomnij” prawdopodobnie nie wykonasz awaitzadania, więc dodawanie jest bezużyteczne ConfigureAwait(false). Jeśli nie zwrócisz awaitwartości zwróconej przez ConfigureAwait, nie może to mieć żadnego efektu.

Po drugie, musisz być świadomy tego, co się dzieje, gdy zadanie kończy się z wyjątkiem. Rozważ proste rozwiązanie, które zasugerował @ ade-miller:

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

Wprowadza to zagrożenie: jeśli nieobsługiwany wyjątek ucieka z SomeMethod(), ten wyjątek nigdy nie zostanie zaobserwowany i może 1 zostać ponownie zgłoszony w wątku finalizatora, powodując awarię aplikacji. Dlatego zalecałbym użycie metody pomocniczej, aby zapewnić przestrzeganie wszelkich wynikających z tego wyjątków.

Możesz napisać coś takiego:

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

Ta implementacja powinna mieć minimalne obciążenie: kontynuacja jest wywoływana tylko wtedy, gdy zadanie nie zakończy się pomyślnie, i powinna być wywoływana synchronicznie (w przeciwieństwie do planowania oddzielnie od oryginalnego zadania). W przypadku „leniwego” nie poniesiesz nawet alokacji dla delegata kontynuacji.

Rozpoczęcie operacji asynchronicznej staje się wtedy trywialne:

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

1. To było domyślne zachowanie w .NET 4.0. W programie .NET 4,5 domyślne zachowanie zostało zmienione w taki sposób, że niezauważone wyjątki nie będą ponownie zgłaszane w wątku finalizatora (chociaż można je nadal obserwować za pośrednictwem zdarzenia UnobservedTaskException w TaskScheduler). Jednak domyślną konfigurację można zastąpić, a nawet jeśli aplikacja wymaga .NET 4.5, nie należy zakładać, że niezauważone wyjątki zadań będą nieszkodliwe.

Mike Strobel
źródło
1
Jeśli procedura obsługi zostanie przekazana, a ContinueWithzostała wywołana z powodu anulowania, właściwość wyjątku poprzednika będzie miała wartość Null i zostanie zgłoszony wyjątek odwołania o wartości null. Ustawienie opcji kontynuacji zadania na OnlyOnFaultedeliminuje potrzebę sprawdzania wartości null lub sprawdzania, czy wyjątek ma wartość null przed jego użyciem.
sanmcp
Metoda Run () musi zostać zastąpiona dla różnych sygnatur metod. Lepiej zrobić to jako rozszerzenie metody Task.
stricq
1
@stricq Pytanie zadane o operację „odpal i zapomnij” (tj. status nigdy nie sprawdzony, a wynik nie zaobserwowany), więc na tym się skupiłem. Kiedy pojawia się pytanie, jak najdelikatniej strzelić sobie w stopę, debata, które rozwiązanie jest „lepsze” z punktu widzenia projektowania, staje się raczej dyskusyjna :). Najlepsza odpowiedź jest zapewne „nie”, ale daleki od minimalnej długości odpowiedzi, więc skupiłem się na dostarczaniu rozwiązań zwięzłą że kwestie obecne w innych unika odpowiedzi. Argumenty są łatwo opakowane w zamknięcie i bez zwracanej wartości, użycie Taskstaje się szczegółem implementacji.
Mike Strobel
@stricq Powiedział, że nie zgadzam się z tobą. Zaproponowana przez ciebie alternatywa jest dokładnie tym, co robiłem w przeszłości, chociaż z różnych powodów. „Chcę uniknąć awarii aplikacji, gdy programista nie zauważy błędu Task” to nie to samo, co „Chcę w prosty sposób uruchomić operację w tle, nigdy nie obserwować jej wyniku i nie obchodzi mnie, jaki mechanizm jest używany aby to zrobić ”. Na tej podstawie popieram swoją odpowiedź na zadane pytanie .
Mike Strobel
Praca w tym przypadku: fire and forgetw ASP.NET WebForms i windows.close()?
PreguntonCojoneroCabrón
7

Aby rozwiązać problem, który wystąpi z odpowiedzią Mike'a Strobela:

Jeśli użyjesz var task = Task.Run(action)i po przypisaniu kontynuacji do tego zadania, pojawi się ryzyko Taskwyrzucenia wyjątku przed przypisaniem kontynuacji obsługi wyjątków do Task. Zatem poniższa klasa powinna być wolna od tego ryzyka:

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

W tym przypadku tasknie jest uruchamiany bezpośrednio, zamiast tego jest tworzony, przypisywana jest kontynuacja, a dopiero potem uruchamiane jest zadanie, aby wyeliminować ryzyko zakończenia wykonywania (lub wyrzucenia wyjątku) przed przypisaniem kontynuacji.

RunMetoda tutaj zwraca kontynuację Task, więc jestem w stanie pisać testy jednostkowe upewniając się, że wykonanie jest kompletny. Możesz jednak bezpiecznie zignorować to w swoim użyciu.

Mert Akcakaya
źródło
Czy to również celowe, że nie uczyniłeś z tego zajęć statycznych?
Deantwo
Ta klasa implementuje interfejs IAsyncManager, więc nie może być klasą statyczną.
Mert Akcakaya
Nie sądzę, aby sytuacja, w której ten problem dotyczy (błędy zadania przed dodaniem kontynuacji), miała znaczenie. Jeśli tak się stanie, stan błędu i wyjątek są przechowywane w zadaniu i będą obsługiwane przez kontynuację po dołączeniu. Ważną kwestią do zrozumienia jest to, że wyjątek nie jest generowany przed await task / task.Wait () / task.Result / task.GetAwaiter (). GetResult () - patrz docs.microsoft.com/en-us/dotnet/ programowanie standardowe / równoległe /… .
Rhys Jones