Найпростіший спосіб зробити метод пожежі і забути в c # 4.0


100

Мені дуже подобається це питання:

Найпростіший спосіб зробити вогонь і забути метод в C #?

Я просто хочу знати, що тепер, коли у нас є розширення Parallel у C # 4.0, чи є кращий чистіший спосіб зробити Fire & Forget за допомогою Parallel linq?


1
Відповідь на це питання все ще стосується .NET 4.0. Пожежа та забудь не стає набагато простішим, ніж QueueUserWorkItem.
Брайан Расмуссен

Відповіді:


108

Не відповідь для 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.

За замовчуванням Завдання намагаються "повернути маршала на початкову нитку", це означає, що ця задача буде виконуватись у фоновому режимі, а потім спробувати повернутися до теми, яка її запустила. Часто стріляйте та забувайте Завдання закінчуються після того, як буде виконана оригінальна нитка. Це призведе до того, що ThreadAbortException буде кинуто. У більшості випадків це нешкідливо - це просто кажу вам, я намагався знову приєднатися, я не зміг, але все одно вам все одно. Але все ще трохи галасливо мати ThreadAbortExceptions або у ваших журналах у виробництві, або у налагоджувачі в локальному розробнику. .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();
}

7
@ksm Ваш підхід, на жаль, має певні проблеми - ви протестували його? Саме такий підхід саме тому існує Warning 4014. Виклик методу асинхронізації без очікування та без допомоги Task.Run ... призведе до запуску цього методу, так, але як тільки він закінчиться, він спробує повернутись до початкового потоку, з якого було випущено. Часто цей потік вже завершив виконання, і ваш код вибухне заплутаними, невизначеними способами. Не робіть цього! Виклик Task.Run - це зручний спосіб сказати "Запустити це у глобальному контексті", не залишаючи нічого для того, щоб зробити спробу маршалку.
Кріс Москіні

1
@ChrisMoschini рада допомогти та дякую за оновлення! З іншого боку, там, де я написав "ви називаєте це з асинхронною анонімною функцією" у коментарі вище, це, чесно, бентежить мене, що є правдою. Мені відомо лише, що код працює, коли асинхронізація не включена до коду виклику (анонімної функції), але чи означає це, що викличний код не буде виконуватися асинхронно (що погано, дуже погано)? Тому я не рекомендую без асинхронізації, просто дивно, що в цьому випадку обидва працюють.
Ніколас Петерсен

8
Я не бачу сенсу ConfigureAwait(false)на Task.Runколи ви не awaitзавдання. Призначення функції в її назві: "configure await ". Якщо ви не виконуєте awaitзавдання, ви не реєструєте продовження, і немає коду для завдання, щоб "повернутися до вихідного потоку", як ви говорите. Більший ризик полягає у тому, що неспостережений виняток буде перекинуто на нитку фіналізатора, чого ця відповідь навіть не стосується.
Mike Strobel

3
@stricq Тут немає жодних асинхронних використання. Якщо ви маєте на увазі async () => ... підпис - це функція, яка повертає завдання, а не недійсне.
Кріс Москіні,

2
На VB.net придушити попередження:#Disable Warning BC42358
Альтьяно Герунг,

85

З Taskкласом так, але PLINQ справді для запитів над колекціями.

Щось наступне зробить це з Task.

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

Або навіть...

Task.Factory.StartNew(FireAway);

Або ...

new Task(FireAway).Start();

Де FireAwayє

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

Отже, в силу стисності імені класу та методу це перевершує версію пулу потоків на шість та дев’ятнадцять символів залежно від вибраного вами :)

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

Звичайно, вони не еквівалентні функціональності?
Джонатан Креснер

6
Існує незначна семантична різниця між StartNew та новим Task.Start, але в іншому випадку, так. Усі вони стоять у черзі FireAway для запуску по потоку в нитці.
Аде Міллер

Працює для цього випадку fire and forget in ASP.NET WebForms and windows.close():?
PreguntonCojoneroCabrón

32

У мене є кілька питань із провідною відповіддю на це питання.

По-перше, у справжній ситуації з пожежею та забуттям ви, мабуть, не 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, ви не повинні вважати, що невиконані винятки завдань будуть нешкідливими.


1
Якщо передано обробник, і його ContinueWithбуло викликано через скасування, властивістю виключення попередника буде Null і буде викинуто виняток null reference. Встановлення параметра продовження завдання OnlyOnFaultedусуне необхідність перевіряти нуль або перевіряти, чи є виняток нулем, перед його використанням.
sanmcp

Метод Run () повинен бути замінений для різних підписів методів. Краще зробити це як метод розширення Завдання.
stricq

1
@stricq Питання про операції "запустити та забути" (тобто статус ніколи не перевірявся, а результат ніколи не спостерігався), тому саме на цьому я зосередився. Коли питання полягає в тому, як найбільш чисто вистрілити собі в ногу, дискусія, яке рішення є "кращим" з точки зору дизайну, стає досить спірною :). Кращий відповідь, можливо , «не», але НЕ виправдав мінімальну довжину відповіді, тому я зосередився на надання короткого рішення , яке дозволить уникнути проблем , присутні в інших відповідях. Аргументи легко загортаються в закриття, і без значення повернення, використання Taskстає деталізацією реалізації.
Mike Strobel

@stricq Тим не менш, я з вами не погоджуюсь. Альтернатива, яку ви запропонували, - це саме те, що я робив у минулому, хоча з різних причин. "Я хочу уникати збоїв програми, коли розробник не помічає помилки Task" - це не те саме, що "Я хочу простий спосіб розпочати фонову операцію, ніколи не спостерігати за її результатом, і мені все одно, який механізм використовується зробити це." На цій підставі я стою за своєю відповіддю на поставлене запитання .
Mike Strobel

Працює в цьому випадку: fire and forgetв ASP.NET WebForms і windows.close()?
PreguntonCojoneroCabrón

7

Просто щоб виправити якусь проблему, яка трапиться з відповіддю Майка Стробеля:

Якщо ви використовуєте 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тому я можу писати модульні тести, переконуючись, що виконання завершено. Ви можете сміливо ігнорувати це у своєму використанні.


Чи це також навмисне, що ви не зробили це статичним класом?
Деантво

Цей клас реалізує інтерфейс IAsyncManager, тому він не може бути статичним класом.
Мерт Аккакая
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.