Самый простой способ сделать огонь и забыть метод в С# 4.0

Мне очень нравится этот вопрос:

Простейший способ сделать огонь и забыть метод в С#?

Я просто хочу знать, что теперь, когда у нас есть параллельные расширения в С# 4.0, есть лучший более чистый способ сделать Fire и Forget с Parallel linq?

Ответ 1

С классом Task да, но PLINQ действительно предназначен для запросов к коллекциям.

Что-то вроде следующего будет делать с помощью Task.

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

Или даже...

Task.Factory.StartNew(FireAway);

Или...

new Task(FireAway).Start();

Где FireAway есть

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

Таким образом, в силу терминологии имени класса и метода это превосходит версию threadpool от шести до девятнадцати символов в зависимости от того, который вы выберете:)

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

Ответ 2

Не ответ для 4.0, но стоит отметить, что в .Net 4.5 вы можете сделать это еще проще:

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

Прагма - отключить предупреждение о том, что вы запускаете эту задачу как огонь и забываете.

Если метод внутри фигурных скобок возвращает задачу:

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

Позвольте сломать это:

Task.Run возвращает задачу, которая генерирует предупреждение о компиляторе (предупреждение CS4014), отмечая, что этот код будет запущен в фоновом режиме - это именно то, что вы хотели, поэтому мы отключили предупреждение 4014.

По умолчанию "Задачи" пытаются "Маршал вернуться к исходному потоку", что означает, что эта задача будет выполняться в фоновом режиме, а затем попытаться вернуться к потоку, который запустил ее. Часто стреляют и забывают Задачи заканчиваются после того, как оригинальная Thread сделана. Это вызовет исключение ThreadAbortException. В большинстве случаев это безвредно - он просто говорит вам, я пытался воссоединиться, я потерпел неудачу, но вам все равно. Но по-прежнему немного шумно иметь ThreadAbortExceptions либо в ваших журналах в Production, либо в вашем отладчике в локальном dev. .ConfigureAwait(false) - это просто способ остаться аккуратным и явно сказать, запустить это в фоновом режиме и что он.

Поскольку это многословие, особенно уродливая прагма, я использую для этого библиотечный метод:

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

Использование:

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

Ответ 3

У меня есть пара вопросов с ведущим ответом на этот вопрос.

Во-первых, в реальной ситуации с огнем и забытьем вы, вероятно, не будете await задачи, поэтому бесполезно добавлять ConfigureAwait(false). Если вы не await значение, возвращаемое ConfigureAwait, то оно не может иметь никакого эффекта.

Во-вторых, вам нужно знать, что происходит, когда задача завершается с исключением. Рассмотрим простое решение, которое предложил @ade-miller:

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

Это создает опасность: если необработанное исключение выходит из SomeMethod(), это исключение никогда не будет соблюдаться и может 1 быть добавлено в поток финализатора, сбой вашего приложения. Поэтому я рекомендую использовать вспомогательный метод, чтобы гарантировать, что все возникающие исключения будут соблюдены.

Вы могли бы написать что-то вроде этого:

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

Эта реализация должна иметь минимальные издержки: продолжение вызывается только в том случае, если задача не завершена успешно, и ее следует вызывать синхронно (в отличие от планирования отдельно от исходной задачи). В "ленивом" случае вы даже не возьмете выделение для делегата продолжения.

Отключение асинхронной операции становится тривиальным:

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

<суб > 1. Это поведение по умолчанию в .NET 4.0. В .NET 4.5 поведение по умолчанию было изменено таким образом, чтобы незаметные исключения не возвращались в поток финализатора (хотя вы все равно можете наблюдать их через событие UnobservedTaskException на TaskScheduler). Однако конфигурацию по умолчанию можно переопределить, и даже если вашему приложению требуется .NET 4.5, вы не должны предполагать, что незаметные исключения задачи будут безвредными.

Ответ 4

Чтобы исправить некоторую проблему, которая случится с Майком Стробелом, ответьте:

Если вы используете var task = Task.Run(action) и после назначения продолжения этой задачи, вы столкнетесь с тем, что Task выделите некоторое исключение, прежде чем назначить продолжение обработчика исключений для Task. Таким образом, класс ниже должен быть свободен от этого риска:

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

Здесь Task не запускается напрямую, вместо этого создается, ему назначается продолжение, и только тогда задача запускается, чтобы исключить риск завершения задачи (или выброса какого-либо исключения) перед назначением продолжение.

Метод Run здесь возвращает продолжение Task, поэтому я могу написать модульные тесты, убедившись, что выполнение завершено. Вы можете смело игнорировать его в своем использовании.