Мне очень нравится этот вопрос:
Простейший способ сделать огонь и забыть метод в С#?
Я просто хочу знать, что теперь, когда у нас есть параллельные расширения в С# 4.0, есть лучший более чистый способ сделать Fire и Forget с Parallel linq?
Мне очень нравится этот вопрос:
Простейший способ сделать огонь и забыть метод в С#?
Я просто хочу знать, что теперь, когда у нас есть параллельные расширения в С# 4.0, есть лучший более чистый способ сделать Fire и Forget с Parallel linq?
С классом 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());
Не ответ для 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();
}
У меня есть пара вопросов с ведущим ответом на этот вопрос.
Во-первых, в реальной ситуации с огнем и забытьем вы, вероятно, не будете 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, вы не должны предполагать, что незаметные исключения задачи будут безвредными.
Чтобы исправить некоторую проблему, которая случится с Майком Стробелом, ответьте:
Если вы используете 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
, поэтому я могу написать модульные тесты, убедившись, что выполнение завершено. Вы можете смело игнорировать его в своем использовании.