La forma más sencilla de hacer un método de incendio y olvido en c # 4.0

Respuestas:

108

No es una respuesta para 4.0, pero vale la pena señalar que en .Net 4.5 puede hacer esto aún más simple con:

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

El pragma es deshabilitar la advertencia que le dice que está ejecutando esta Tarea como disparar y olvidar.

Si el método dentro de las llaves devuelve una tarea:

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

Analicemos eso:

Task.Run devuelve una tarea, que genera una advertencia del compilador (advertencia CS4014) que indica que este código se ejecutará en segundo plano; eso es exactamente lo que deseaba, por lo que deshabilitamos la advertencia 4014.

De forma predeterminada, las tareas intentan "Marshal de nuevo en el subproceso original", lo que significa que esta tarea se ejecutará en segundo plano y luego intentará volver al subproceso que lo inició. A menudo, las Tareas de disparo y olvido finalizan una vez finalizado el hilo original. Eso hará que se lance una ThreadAbortException. En la mayoría de los casos, esto es inofensivo, solo te dice que intenté unirme, fallé, pero de todos modos no te importa. Pero todavía es un poco ruidoso tener ThreadAbortExceptions en sus registros en Producción o en su depurador en el desarrollo local..ConfigureAwait(false)es solo una forma de mantenerse ordenado y decir explícitamente, ejecute esto en segundo plano, y eso es todo.

Como esto es prolijo, especialmente el pragma feo, utilizo un método de biblioteca para esto:

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

Uso:

TaskHelper.RunBg(async () =>
{
    await doSomethingAsync();
}
Chris Moschini
fuente
7
@ksm Desafortunadamente, su enfoque tiene algunos problemas, ¿lo ha probado? Ese enfoque es exactamente la razón por la que existe la Advertencia 4014. Llamar a un método asíncrono sin esperar, y sin la ayuda de Task.Run ... hará que ese método se ejecute, sí, pero una vez que finalice, intentará volver al hilo original desde el que se disparó. A menudo, ese hilo ya habrá completado la ejecución y su código explotará de manera confusa e indeterminada. ¡No lo hagas! La llamada a Task.Run es una forma conveniente de decir "Ejecutar esto en el contexto global", sin dejar nada para que intente ordenar.
Chris Moschini
1
@ChrisMoschini encantado de ayudar, ¡y gracias por la actualización! En el otro asunto, donde escribí "lo estás llamando con una función anónima asincrónica" en el comentario anterior, es honestamente confuso para mí cuál es la verdad. Todo lo que sé es que el código funciona cuando el async no está incluido en el código de llamada (función anónima), pero ¿eso significaría que el código de llamada no se ejecutaría de forma asincrónica (lo cual es malo, un error muy malo)? Así que no recomiendo sin el async, es extraño que en este caso ambos funcionen.
Nicholas Petersen
8
No veo el punto de ConfigureAwait(false)en Task.Runcuando no lo hace awaitla tarea. El propósito de la función está en su nombre: "configure await ". Si no realiza awaitla tarea, no registra una continuación y no hay ningún código para que la tarea "vuelva al hilo original", como dice. El mayor riesgo es que se vuelva a lanzar una excepción no observada en el hilo del finalizador, que esta respuesta ni siquiera aborda.
Mike Strobel
3
@stricq No hay usos de vacío asíncrono aquí. Si te refieres a async () => ... la firma es un Func que devuelve una Tarea, no nula.
Chris Moschini
2
En VB.net para suprimir la advertencia:#Disable Warning BC42358
Altiano Gerung
85

Con la Taskclase sí, pero PLINQ es realmente para consultar colecciones.

Algo como lo siguiente lo hará con Task.

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

O incluso...

Task.Factory.StartNew(FireAway);

O...

new Task(FireAway).Start();

Donde FireAwayesta

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

Entonces, en virtud de la concisión del nombre de la clase y el método, esto supera a la versión de threadpool entre seis y diecinueve caracteres, según el que elija :)

ThreadPool.QueueUserWorkItem(o => FireAway());
Ade Miller
fuente
¿Sin embargo, seguramente no son funcionalidades equivalentes?
Jonathon Kresner
6
Existe una sutil diferencia semántica entre StartNew y new Task.Start, pero de lo contrario, sí. Todos ponen en cola FireAway para que se ejecute en un subproceso del grupo de subprocesos.
Ade Miller
Trabajando para este caso fire and forget in ASP.NET WebForms and windows.close():?
PreguntonCojoneroCabrón
32

Tengo un par de problemas con la respuesta principal a esta pregunta.

En primer lugar, en una situación real de disparar y olvidar , probablemente no se hará awaitla tarea, por lo que es inútil agregar ConfigureAwait(false). Si no lo hace, awaitel valor devuelto porConfigureAwait , no es posible que tenga ningún efecto.

En segundo lugar, debe estar al tanto de lo que sucede cuando la tarea se completa con una excepción. Considere la solución simple que sugirió @ ade-miller:

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

Esto introduce un peligro: si una excepción no controlada se escapa de SomeMethod(), nunca se podrá observar que la excepción, y pueden 1 pueden volver a generar en el subproceso finalizador, bloquear su aplicación. Por lo tanto, recomendaría usar un método auxiliar para garantizar que se observen las excepciones resultantes.

Podrías escribir algo como esto:

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

Esta implementación debe tener una sobrecarga mínima: la continuación solo se invoca si la tarea no se completa correctamente y debe invocarse de forma sincrónica (en lugar de programarse por separado de la tarea original). En el caso "perezoso", ni siquiera incurrirá en una asignación para el delegado de continuación.

Iniciar una operación asincrónica se vuelve trivial:

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

1. Este era el comportamiento predeterminado en .NET 4.0. En .NET 4.5, el comportamiento predeterminado se cambió de modo que las excepciones no observadas no se volvieran a lanzar en el hilo del finalizador (aunque aún puede observarlas a través del evento UnobservedTaskException en TaskScheduler). Sin embargo, la configuración predeterminada se puede anular, e incluso si su aplicación requiere .NET 4.5, no debe asumir que las excepciones de tareas no observadas serán inofensivas.

Mike Strobel
fuente
1
Si se pasa un controlador y ContinueWithse invoca debido a la cancelación, la propiedad de excepción del antecedente será Null y se lanzará una excepción de referencia nula. Establecer la opción de continuación de la tarea en OnlyOnFaultedeliminará la necesidad de la verificación nula o comprobará si la excepción es nula antes de usarla.
sanmcp
El método Run () debe anularse para diferentes firmas de métodos. Es mejor hacer esto como un método de extensión de Task.
stricq
1
@stricq La pregunta se refería a las operaciones de "disparar y olvidar" (es decir, el estado nunca se verificó y el resultado nunca se observó), así que en eso me concentré. Cuando la pregunta es cómo dispararse uno mismo en el pie de la manera más limpia, debatir qué solución es "mejor" desde la perspectiva del diseño se vuelve bastante discutible :). La mejor respuesta es sin duda "no", pero que no llegó a la longitud de la respuesta mínima, por lo que me he centrado en proporcionar una solución concisa que las cuestiones Evita presentan en otras respuestas. Los argumentos se envuelven fácilmente en un cierre y, sin valor de retorno, el uso se Taskconvierte en un detalle de implementación.
Mike Strobel
@stricq Dicho esto, no estoy en desacuerdo contigo. La alternativa que propuso es exactamente lo que hice en el pasado, aunque por diferentes razones. "Quiero evitar que la aplicación se cuelgue cuando un desarrollador no observa una falla Task" no es lo mismo que "Quiero una forma sencilla de iniciar una operación en segundo plano, nunca observar su resultado y no me importa qué mecanismo se utilice para hacerlo." Sobre esa base, respaldo mi respuesta a la pregunta que se hizo .
Mike Strobel
Trabajando para este caso: fire and forgeten ASP.NET WebForms y windows.close()?
PreguntonCojoneroCabrón
7

Solo para solucionar algún problema que sucederá con la respuesta de Mike Strobel:

Si usa var task = Task.Run(action)y luego asigna una continuación a esa tarea, correrá el riesgo de Tasklanzar alguna excepción antes de asignar una continuación del controlador de excepciones al Task. Por lo tanto, la clase siguiente debería estar libre de este riesgo:

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

Aquí, taskno se ejecuta directamente, sino que se crea, se asigna una continuación y solo entonces se ejecuta la tarea para eliminar el riesgo de que la tarea complete la ejecución (o arroje alguna excepción) antes de asignar una continuación.

El Runmétodo aquí devuelve la continuación, Taskpor lo que puedo escribir pruebas unitarias asegurándome de que la ejecución esté completa. Sin embargo, puede ignorarlo con seguridad en su uso.

Mert Akcakaya
fuente
¿También es intencional que no la hayas convertido en una clase estática?
Deantwo
Esta clase implementa la interfaz IAsyncManager, por lo que no puede ser una clase estática.
Mert Akcakaya