在c#4.0中执行fire and forget方法的最简单方法

Jon*_*ner 86 .net c# .net-4.0 task-parallel-library fire-and-forget

我真的很喜欢这个问题:

在C#中执行fire and forget方法的最简单方法是什么?

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

Chr*_*ini 90

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

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

编译指示是禁用警告,告诉您正在运行此任务作为火灾并忘记.

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

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

让我们打破这个:

Task.Run返回一个Task,它生成一个编译器警告(警告CS4014),注意到这个代码将在后台运行 - 这正是你想要的,所以我们禁用警告4014.

默认情况下,Tasks尝试"Marshal回到原始线程",这意味着此任务将在后台运行,然后尝试返回启动它的线程.经常解雇并忘记在完成原始Thread之后完成任务.这将导致抛出ThreadAbortException.在大多数情况下,这是无害的 - 它只是告诉你,我试图重新加入,我失败了,但你无论如何都不在乎.但是,在生产中的日志或本地开发人员的调试器中使用ThreadAbortExceptions仍然有点吵..ConfigureAwait(false)只是一种保持整洁的方式,明确地说,在后台运行它,就是这样.

由于这是罗嗦,特别是丑陋的pragma,我使用库方法:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

TaskHelper.RunBg(async () =>
{
    await doSomethingAsync();
}
Run Code Online (Sandbox Code Playgroud)

  • 当你没有"等待"任务时,我没有在`Task.Run`上看到`ConfigureAwait(false)`的意思.该函数的目的在于其名称:"configure _await_".如果您没有"等待"任务,则不会注册延续,并且没有任何代码可以"重新组织回原始线程",如您所说.更大的风险是在终结器线程上重新抛出一个未观察到的异常,这个答案甚至都没有解决. (7认同)
  • @ksm不幸的是你的方法遇到了麻烦 - 你测试过了吗?这种方法正是警告4014存在的原因.在没有等待的情况下调用异步方法,并且没有Task.Run的帮助...将导致该方法运行,是的,但是一旦完成它将尝试编组回到它被触发的原始​​线程.通常该线程已经完成执行,您的代码将以令人困惑,不确定的方式爆炸.不要这样做!对Task.Run的调用是一种方便的方式,可以说"在全局环境中运行它",没有任何东西让它试图编组. (6认同)
  • @stricq这里没有异步void使用.如果您指的是async()=> ...签名是一个Func,它返回一个Task,而不是void. (3认同)
  • 在 VB.net 上抑制警告:`#Disable Warning BC42358` (3认同)
  • @AlexeyStrakh 认为你把这个问题复杂化了。你实际上只需要在那里放 try/catch 。无需依赖其他开发人员。`Task.Run(async () =&gt; { try { await MyFireAndForgetMethod(); } catch(... {...} }).ConfigureAwait(false);` (2认同)
  • 在 C# 7 中,您可以使用 Discards (_) 并且无需抑制警告 (2认同)

Ade*_*ler 83

对于Task类是,但PLINQ实际上是用于查询集合.

以下内容将使用Task执行此操作.

Task.Factory.StartNew(() => FireAway());
Run Code Online (Sandbox Code Playgroud)

甚至...

Task.Factory.StartNew(FireAway);
Run Code Online (Sandbox Code Playgroud)

要么...

new Task(FireAway).Start();
Run Code Online (Sandbox Code Playgroud)

哪里FireAway

public static void FireAway()
{
    // Blah...
}
Run Code Online (Sandbox Code Playgroud)

因此,凭借类和方法名称的简洁性,这取决于您选择的那个,使得线程池版本超过6到19个字符:)

ThreadPool.QueueUserWorkItem(o => FireAway());
Run Code Online (Sandbox Code Playgroud)

  • StartNew和新的Task.Start之间存在细微的语义差异,但是,是的.它们都将FireAway排队以在线程池中的线程上运行. (6认同)

Mik*_*bel 26

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

首先,在一个真正的发生火灾的情况下,你可能不会await完成任务,所以追加是没用的ConfigureAwait(false).如果你没有await返回的值ConfigureAwait,那么它可能不会有任何影响.

其次,您需要了解任务以异常完成时会发生什么.考虑@ ade-miller建议的简单解决方案:

Task.Factory.StartNew(SomeMethod);  // .NET 4.0
Task.Run(SomeMethod);               // .NET 4.5
Run Code Online (Sandbox Code Playgroud)

这引入了一个危险:如果未处理的异常从逃逸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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

此实现应该具有最小的开销:仅在任务未成功完成时才调用继续,并且应该同步调用它(而不是与原始任务分开调度).在"懒惰"的情况下,您甚至不会为继续委托进行分配.

开始异步操作然后变得微不足道:

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

1.这是.NET 4.0中的默认行为.在.NET 4.5中,更改了默认行为,以便在终结器线程上不会重新抛出观察到的异常(尽管您仍可以通过TaskScheduler上的UnobservedTaskException事件来观察它们).但是,可以覆盖默认配置,即使您的应用程序需要.NET 4.5,也不应该假设未观察到的任务异常将是无害的.

  • 如果传入处理程序并由于取消而调用“ContinueWith”,则前件的异常属性将为 Null,并且将引发空引用异常。将任务继续选项设置为“OnlyOnFaulted”将消除在使用异常之前进行 null 检查或检查异常是否为 null 的需要。 (2认同)
  • @stricq 这个问题询问了“即发即忘”操作(即状态从未检查且结果从未观察到),所以这就是我关注的重点。当问题是如何最干净地搬起石头砸自己的脚时,从设计角度争论哪种解决方案“更好”就变得毫无意义了:)。_最佳_答案可以说是“不”,但这没有达到最小答案长度,因此我专注于提供一个简洁的解决方案,以避免其他答案中出现的问题。参数很容易包装在闭包中,并且没有返回值,使用“Task”成为实现细节。 (2认同)

Mer*_*aya 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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在这里 task不是直接运行,而是创建了,分配了延续,然后才运行任务,以消除在分配延续之前任务完成执行(或引发某些异常)的风险。

Run这里的方法返回延续,Task因此我能够编写单元测试以确保执行完成。不过,您可以放心使用它。