Il modo più semplice per eseguire un fuoco e dimenticare il metodo in c # 4.0


100

Mi piace molto questa domanda:

Il modo più semplice per eseguire un fuoco e dimenticare il metodo in C #?

Voglio solo sapere che ora che abbiamo estensioni parallele in C # 4.0 esiste un modo migliore e più pulito per eseguire Fire & Forget con Parallel linq?


1
La risposta a questa domanda si applica ancora a .NET 4.0. Spara e dimentica non diventa molto più semplice di QueueUserWorkItem.
Brian Rasmussen

Risposte:


108

Non è una risposta per 4.0, ma vale la pena notare che in .Net 4.5 puoi renderlo ancora più semplice con:

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

Il pragma è disabilitare l'avviso che ti dice che stai eseguendo questa attività come spara e dimentica.

Se il metodo all'interno delle parentesi graffe restituisce un'attività:

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

Analizziamolo:

Task.Run restituisce un'attività, che genera un avviso del compilatore (avviso CS4014) notando che questo codice verrà eseguito in background: è esattamente quello che volevi, quindi disabilitiamo l'avviso 4014.

Per impostazione predefinita, le attività tentano di "eseguire il marshalling di nuovo sul thread originale", il che significa che questa attività verrà eseguita in background, quindi tenterà di tornare al thread che l'ha avviata. Spesso spara e dimentica che le attività finiscono dopo che il thread originale è stato completato. Ciò causerà la generazione di un'eccezione ThreadAbortException. Nella maggior parte dei casi questo è innocuo: ti sto solo dicendo che ho provato a ricongiungermi, ho fallito, ma non ti interessa comunque. Ma è ancora un po 'rumoroso avere ThreadAbortExceptions nei tuoi log in Produzione o nel tuo debugger nello sviluppo locale. .ConfigureAwait(false)è solo un modo per rimanere in ordine e dire esplicitamente, eseguilo in background, e basta.

Poiché questo è prolisso, specialmente il brutto pragma, uso un metodo di libreria per questo:

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

Utilizzo:

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

7
@ksm Il tuo approccio ha dei problemi purtroppo - l'hai testato? Questo approccio è esattamente il motivo per cui esiste l'Avviso 4014. Chiamare un metodo asincrono senza await e senza l'aiuto di Task.Run ... provocherà l'esecuzione di quel metodo, sì, ma una volta terminato tenterà di eseguire il marshalling al thread originale da cui è stato attivato. Spesso quel thread avrà già completato l'esecuzione e il tuo codice esploderà in modi confusi e indeterminati. Non farlo! La chiamata a Task.Run è un modo conveniente per dire "Esegui questo nel contesto globale", senza lasciare nulla a cui tentare di eseguire il marshalling.
Chris Moschini

1
@ChrisMoschini felice di aiutare e grazie per l'aggiornamento! D'altra parte, dove ho scritto "lo chiami con una funzione anonima asincrona" nel commento sopra, è sinceramente confuso per me che è la verità. Tutto quello che so è che il codice funziona quando l'asincrono non è incluso nel codice chiamante (funzione anonima), ma ciò significherebbe che il codice chiamante non verrebbe eseguito in modo asincrono (il che è negativo, un pessimo gotcha)? Così sto , non raccomandando senza l'asincrono, è solo strano che in questo caso, sia il lavoro.
Nicholas Petersen

8
Non vedo il punto di ConfigureAwait(false)su Task.Runquando non lo fai awaitil compito. Lo scopo della funzione è nel suo nome: "configure await ". Se non si esegue awaitl'attività, non si registra una continuazione e non esiste alcun codice per l'attività per "eseguire il marshalling di nuovo sul thread originale", come si dice. Il rischio maggiore è che un'eccezione non osservata venga rilanciata sul thread del finalizzatore, che questa risposta non affronta nemmeno.
Mike Strobel

3
@stricq Non ci sono usi void asincroni qui. Se ti riferisci ad async () => ... la firma è una Func che restituisce un Task, non void.
Chris Moschini

2
Su VB.net per sopprimere l'avviso:#Disable Warning BC42358
Altiano Gerung

85

Con la Taskclasse sì, ma PLINQ è davvero per eseguire query sulle raccolte.

Qualcosa di simile al seguente lo farà con Task.

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

O anche...

Task.Factory.StartNew(FireAway);

O...

new Task(FireAway).Start();

dove FireAwaysi trova

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

Quindi, in virtù della concisione dei nomi di classe e metodo, questo batte la versione del pool di thread da sei a diciannove caratteri a seconda di quello che scegli :)

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

Sicuramente non sono comunque funzionalità equivalenti?
Jonathon Kresner

6
C'è una sottile differenza semantica tra StartNew e new Task.Start ma per il resto sì. Tutti accodano FireAway per essere eseguito su un thread nel pool di thread.
Ade Miller

Lavorando per questo caso fire and forget in ASP.NET WebForms and windows.close():?
PreguntonCojoneroCabrón

32

Ho un paio di problemi con la risposta principale a questa domanda.

Primo, in una vera situazione di fuoco e dimentica , probabilmente non avrai awaitil compito, quindi è inutile aggiungere ConfigureAwait(false). Se non si utilizza awaitil valore restituito da ConfigureAwait, non può avere alcun effetto.

In secondo luogo, è necessario essere consapevoli di ciò che accade quando l'attività viene completata con un'eccezione. Considera la semplice soluzione suggerita da @ ade-miller:

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

Ciò introduce un pericolo: se un'eccezione non gestita sfugge SomeMethod(), quell'eccezione non verrà mai osservata e potrebbe essere 1 lanciata di nuovo sul thread del finalizzatore, bloccando l'applicazione. Suggerirei quindi di utilizzare un metodo di supporto per garantire che tutte le eccezioni risultanti vengano osservate.

Potresti scrivere qualcosa del genere:

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

Questa implementazione dovrebbe avere un sovraccarico minimo: la continuazione viene richiamata solo se l'attività non viene completata correttamente e dovrebbe essere richiamata in modo sincrono (invece di essere pianificata separatamente dall'attività originale). Nel caso "pigro", non dovrai nemmeno sostenere un'allocazione per il delegato di continuazione.

Avviare un'operazione asincrona diventa quindi banale:

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

1. Questo era il comportamento predefinito in .NET 4.0. In .NET 4.5, il comportamento predefinito è stato modificato in modo tale che le eccezioni non osservate non venissero lanciate di nuovo sul thread del finalizzatore (sebbene sia ancora possibile osservarle tramite l'evento UnobservedTaskException su TaskScheduler). Tuttavia, la configurazione predefinita può essere sovrascritta e, anche se l'applicazione richiede .NET 4.5, non si dovrebbe presumere che le eccezioni di attività non osservate saranno innocue.


1
Se viene passato un gestore e ContinueWithviene richiamato a causa dell'annullamento, la proprietà dell'eccezione antecedente sarà Null e verrà generata un'eccezione di riferimento null. L'impostazione dell'opzione di continuazione dell'attività su OnlyOnFaultedeliminerà la necessità del controllo null o controllerà se l'eccezione è nulla prima di utilizzarla.
sanmcp

Il metodo Run () deve essere sovrascritto per firme di metodi differenti. È meglio farlo come metodo di estensione di Task.
rigoroso

1
@stricq La domanda posta sulle operazioni "spara e dimentica" (cioè lo stato non è mai stato verificato e il risultato non è mai stato osservato), quindi è su questo che mi sono concentrato. Quando la domanda è come spararsi in modo più pulito ai piedi, discutere su quale soluzione sia "migliore" dal punto di vista del design diventa piuttosto discutibile :). La risposta migliore è probabilmente "non", ma non è stata raggiunta la lunghezza minima della risposta, quindi mi sono concentrato sul fornire una soluzione concisa che eviti i problemi presenti in altre risposte. Gli argomenti sono facilmente racchiusi in una chiusura e, senza alcun valore di ritorno, l'utilizzo Taskdiventa un dettaglio di implementazione.
Mike Strobel

@stricq Detto questo, non sono in disaccordo con te. L'alternativa che hai proposto è esattamente quella che ho fatto in passato, anche se per motivi diversi. "Voglio evitare l'arresto anomalo dell'app quando uno sviluppatore non riesce a osservare un errore Task" non è la stessa cosa di "Voglio un modo semplice per avviare un'operazione in background, non osservarne mai il risultato e non mi interessa quale meccanismo viene utilizzato per farlo." Su questa base, sostengo la mia risposta alla domanda che è stata posta .
Mike Strobel

Funzionando per questo caso: fire and forgetin ASP.NET WebForms e windows.close()?
PreguntonCojoneroCabrón

7

Solo per risolvere alcuni problemi che si verificheranno con la risposta di Mike Strobel:

Se si utilizza var task = Task.Run(action)e dopo si assegna una continuazione a tale attività, si correrà il rischio di Taskgenerare qualche eccezione prima di assegnare una continuazione del gestore di eccezioni a Task. Quindi, la classe seguente dovrebbe essere esente da questo rischio:

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

Qui, tasknon viene eseguito direttamente, ma viene creato, viene assegnata una continuazione e solo allora l'attività viene eseguita per eliminare il rischio che l'attività completi l'esecuzione (o generi qualche eccezione) prima di assegnare una continuazione.

Il Runmetodo qui restituisce la continuazione, Taskquindi sono in grado di scrivere unit test assicurandomi che l'esecuzione sia completa. Puoi tranquillamente ignorarlo nel tuo utilizzo.


È anche intenzionale che tu non l'abbia reso una classe statica?
Deantwo

Questa classe implementa l'interfaccia IAsyncManager, quindi non può essere una classe statica.
Mert Akcakaya
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.