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

Posted

技术标签:

【中文标题】在 c# 4.0 中执行火灾和忘记方法的最简单方法【英文标题】:Simplest way to do a fire and forget method in c# 4.0 【发布时间】:2022-01-13 16:00:28 【问题描述】:

我真的很喜欢这个问题:

Simplest way to do a fire and forget method in C#?

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

【问题讨论】:

该问题的答案仍然适用于 .NET 4.0。 Fire and forget 并没有比 QueueUserWorkItem 简单得多。 这能回答你的问题吗? Simplest way to do a fire and forget method in C#? 【参考方案1】:

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

#pragma warning disable 4014
Task.Run(() =>

    MyFireAndForgetMethod();
).ConfigureAwait(false);
#pragma warning restore 4014

pragma 是禁用警告,告诉您您正在运行此任务,然后忘记。

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

#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();

【讨论】:

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

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());

【讨论】:

当然它们的功能不等同? StartNew 和 new Task.Start 之间存在细微的语义差异,但除此之外,是的。它们都将 FireAway 排队以在线程池中的线程上运行。 为这个案例工作:fire and forget in ASP.NET WebForms and windows.close() ?【参考方案3】:

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

首先,在真正的即发即弃情况下,您可能不会await 任务,因此附加ConfigureAwait(false) 是没有用的。如果你不awaitConfigureAwait返回的值,那么它不可能有任何效果。

其次,您需要了解当任务以异常完成时会发生什么。考虑@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,您也不应该假设未观察到的任务异常是无害的。

【讨论】:

如果传入处理程序,并且由于取消而调用了ContinueWith,则前件的异常属性将为Null,并引发空引用异常。将任务继续选项设置为OnlyOnFaulted 将无需进行空检查或在使用异常之前检查异常是否为空。 Run() 方法需要被不同的方法签名覆盖。最好将其作为 Task 的扩展方法。 @stricq 这个问题是关于“一劳永逸”操作的(即从未检查过状态并且从未观察到结果),所以这就是我关注的问题。当问题是如何最干净地射击自己时,从设计的角度争论哪种解决方案“更好”变得相当没有意义:)。 最佳 答案可以说是“不要”,但这没有达到最短答案长度,因此我专注于提供一个简洁的解决方案,以避免其他答案中出现问题。参数很容易包装在闭包中,并且没有返回值,使用Task 成为实现细节。 @stricq 也就是说,我不同意你的观点。您提出的替代方案正是我过去所做的,尽管出于不同的原因。 “我想避免在开发人员未能观察到故障Task 时导致应用程序崩溃”与“我想要一种简单的方法来启动后台操作,从不观察其结果,我不在乎什么机制是用来做的。”在此基础上,我支持我的回答对所提出的问题 适用于这种情况:fire and forget in ASP.NET WebFormswindows.close() ?【参考方案4】:

只是为了解决 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 的延续,因此我可以编写单元测试以确保执行完成。不过,您可以放心地在使用时忽略它。

【讨论】:

你是不是故意不把它变成静态类? 这个类实现了IAsyncManager接口,所以不能是静态类。 我认为解决的情况(添加继续之前的任务错误)并不重要。如果发生这种情况,故障状态和异常将存储在任务中,并在附加时由延续处理。要理解的重要一点是,在等待任务/task.Wait()/task.Result/task.GetAwaiter().GetResult() 之前不会引发异常 - 请参阅docs.microsoft.com/en-us/dotnet/standard/parallel-programming/…。 不要吞下异常,记录它。 如果它实际上没有运行任务,可能不应该称之为Run :)。

以上是关于在 c# 4.0 中执行火灾和忘记方法的最简单方法的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 中处理关联数组的最简单方法是啥?

在 C# 中比较数组的最简单方法

在 C# 字符串中摆脱零宽度空间的最简单方法

使用 IPC 在 Python 和 C# 之间进行通信的最简单方法?

在 C# 中旋转列表的最简单方法

保存和加载信息的最简单方法c#