在C#4.0中执行“忘却”方法的最简单方法


100

我真的很喜欢这个问题:

在C#中最简单的方法来做一个“忘了”的方法?

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


1
该问题的答案仍然适用于.NET 4.0。抛弃并没有比QueueUserWorkItem简单得多。
Brian Rasmussen

Answers:


108

这不是4.0的答案,但是值得注意的是,在.Net 4.5中,您可以使用以下方法使此过程更加简单:

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

实用程序是禁用警告,该警告告诉您您正在运行此任务,而忘记了。

如果花括号内的方法返回Task:

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

让我们分解一下:

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

默认情况下,Tasks尝试“编组回原始线程”,这意味着此Task将在后台运行,然后尝试返回到启动它的线程。通常,当执行完原始线程后,便会忘记执行任务。这将导致引发ThreadAbortException。在大多数情况下,这是无害的-只是告诉您,我尝试重新加入,但失败了,但是您无论如何都不在乎。但是,无论是在Production的日志中还是在本地dev的调试器中都具有ThreadAbortExceptions,仍然有点麻烦。.ConfigureAwait(false)只是保持整洁并明确地说在后台运行该程序的一种方法,仅此而已。

由于这比较罗word,尤其是丑陋的编译指示,因此我使用了一种库方法:

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不幸的是,您的方法遇到了一些麻烦-您是否测试过?该方法正是警告4014存在的原因。在没有等待的情况下并且没有Task.Run ...的情况下调用异步方法将导致该方法运行,是的,但是一旦完成,它将尝试编组回到触发它的原始线程。通常,该线程将已经完成执行,并且您的代码将以混乱,不确定的方式爆炸。别做!对Task.Run的调用是一种方便的说法,即“在全局上下文中运行”,不留任何试图编组的方法。
克里斯·莫斯基尼

1
@ChrisMoschini很高兴为您提供帮助,并感谢您的更新!另一方面,在上面的评论中我写了“您正在使用异步匿名函数调用它”,这确实使我感到困惑,这是事实。我所知道的是,当异步代码未包含在调用代码(匿名函数)中时,代码会起作用,但这是否意味着调用代码不会以异步方式运行(这很糟糕,很糟糕)。因此,我无异步推荐,它只是奇怪的是,在这种情况下,无论是工作。
Nicholas Petersen

8
我不明白的点ConfigureAwait(false)Task.Run,当你不知道await的事。该功能的目的在于其名称:“ configure await ”。如果您没有执行await该任务,则不会注册延续,并且没有该任务的代码可以将其“编组回原始线程”。更大的风险是在终结器线程上引发了一个未被观察到的异常,这个答案甚至没有解决。
Mike Strobel

3
@stricq这里没有异步void用法。如果您指的是async()=> ...,则签名是一个返回Task的Func,而不是void。
克里斯·莫斯基尼

2
在VB.net上发出警告警告:#Disable Warning BC42358
Altiano Gerung

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和new 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中,默认行为已更改,使得未观察到的异常会不会重新抛出终结线程(虽然你仍然可以通过上的TaskScheduler的UnobservedTaskException事件观察它们)。但是,默认配置可以被覆盖,即使您的应用程序需要.NET 4.5,您也不应假定未观察到的任务异常将是无害的。


1
如果传入了一个处理程序,并且ContinueWith由于取消而调用了该处理程序,那么前者的exception属性将为Null,并且将引发null引用异常。将任务继续选项设置为OnlyOnFaulted将会消除对null检查的需要,或者在使用它之前检查异常是否为null。
sanmcp '16

对于不同的方法签名,需要重写Run()方法。最好将其作为Task的扩展方法来进行。
stricq

1
@stricq这个问题被问到“解雇”操作(即,从未检查过状态并且从未观察到结果),所以这就是我关注的重点。当问题是如何最清晰地射击自己的脚时,从设计的角度争论哪种解决方案“更好”就变得很无聊了:)。在最好的答案可以说是“不”,但是这短短的最小长度的答案下跌,所以我专注于提供一个简洁的解决方案,避免问题出现在其他的答案。使用Task成为实现细节,参数很容易包装在闭包中,并且没有返回值。
Mike Strobel

@stricq就是说,我不同意你的看法。您提出的替代方案正是我过去所做的,尽管出于不同的原因。“我希望避免在开发人员无法观察到故障时使应用程序崩溃Task”与“我想要一种简单的方法来启动后台操作,从不观察其结果,并且我不在乎使用哪种机制去做吧。” 在此基础上,我支持对所提问题的回答
Mike Strobel

针对这种情况的工作:fire and forgetASP.NET WebFormswindows.close()?中。
PreguntonCojoneroCabrón

7

只是为了解决Mike Strobel的答案可能发生的一些问题:

如果您使用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因此我能够编写单元测试以确保执行完成。不过,您可以放心使用它。


您不是故意将它设为静态类吗?
Deantwo

此类实现IAsyncManager接口,因此不能为静态类。
Mert Akcakaya
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.