113

我真的很喜欢这个问题:

在 C# 中执行火灾和忘记方法的最简单方法?

我只想知道,既然我们在 C# 4.0 中有并行扩展,是否有更好的清洁方法来使用并行 linq 执行 Fire & Forget?

4

4 回答 4

127

不是 4.0 的答案,但值得注意的是,在 .Net 4.5 中,您可以通过以下方式使其更简单:

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

pragma 是禁用警告,告诉您正在运行此任务作为“一劳永逸”。

如果花括号内的方法返回一个任务:

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

让我们分解一下:

Task.Run 返回一个任务,它会生成一个编译器警告(警告 CS4014),指出此代码将在后台运行 - 这正是您想要的,因此我们禁用警告 4014。

默认情况下,Tasks 会尝试“Marshal back to the original Thread”,这意味着此 Task 将在后台运行,然后尝试返回启动它的 Thread。通常在原始线程完成后触发并忘记任务完成。这将导致抛出 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();
}
于 2013-12-03T23:41:52.237 回答
89

类是的Task,但 PLINQ 确实是用于查询集合。

像下面这样的东西可以用 Task 来完成。

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

甚至...

Task.Factory.StartNew(FireAway);

或者...

new Task(FireAway).Start();

FireAway在哪里

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

因此,凭借类和方法名称的简洁性,这比线程池版本高出 6 到 19 个字符,具体取决于您选择的字符:)

ThreadPool.QueueUserWorkItem(o => FireAway());
于 2011-04-11T02:54:29.470 回答
40

我对这个问题的主要答案有几个问题。

首先,在真正的即发即弃的情况下,您可能不会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 中,更改了默认行为,以便不会在终结器线程上重新抛出未观察到的异常尽管您仍然可以通过 TaskScheduler 上的 UnobservedTaskException 事件观察它们)。但是,可以覆盖默认配置,即使您的应用程序需要 .NET 4.5,您也不应该假设未观察到的任务异常是无害的。

于 2016-06-29T13:55:13.727 回答
9

只是为了解决 Mike Strobel 的回答会发生的一些问题:

如果var task = Task.Run(action)在为该任务分配延续之后使用 and ,那么在将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,因此我可以编写单元测试以确保执行完成。不过,您可以放心地在使用中忽略它。

于 2017-11-10T09:54:09.290 回答