【问题标题】:Fire-and-forget with async vs "old async delegate"异步与“旧异步委托”的一劳永逸
【发布时间】:2012-10-09 15:04:31
【问题描述】:

我正在尝试用一种新语法替换我旧的即发即弃调用,希望更简单,但它似乎让我望而却步。这是一个例子

class Program
{
    static void DoIt(string entry) 
    { 
        Console.WriteLine("Message: " + entry);
    }

    static async void DoIt2(string entry)
    {
        await Task.Yield();
        Console.WriteLine("Message2: " + entry);
    }

    static void Main(string[] args)
    {
        // old way
        Action<string> async = DoIt;
        async.BeginInvoke("Test", ar => { async.EndInvoke(ar); ar.AsyncWaitHandle.Close(); }, null);
        Console.WriteLine("old-way main thread invoker finished");
        // new way
        DoIt2("Test2");   
        Console.WriteLine("new-way main thread invoker finished");
        Console.ReadLine();
    }
}

两种方法都做同样的事情,但是我似乎已经获得了(不需要EndInvoke 并关闭句柄,这仍然有点值得商榷)我不得不等待@987654324 以新的方式失败@,这实际上带来了一个新问题,即必须重写所有现有的异步 F&F 方法才能添加该单行。在性能/清理方面是否有一些看不见的收益?

如果我无法修改后台方法,我将如何应用异步?在我看来,没有直接的方法,我必须创建一个等待 Task.Run() 的包装异步方法?

编辑:我现在看到我可能错过了一个真正的问题。问题是:给定一个同步方法 A(),我如何才能使用async/await 以即发即弃的方式异步调用它,而不会得到比“旧方法”更复杂的解决方案

【问题讨论】:

  • async/await 并不是真正为将同步工作负载卸载到另一个线程而设计的。我在一些非常庞大的项目中使用了 async/await,但看不到 Thread.Yield。我认为这段代码滥用了异步等待哲学。如果没有异步 IO,async/await 可能是错误的解决方案。
  • 我不同意,尤其是在我的情况下;没有合理的理由强制 http 请求者等待一个完整的过程完成以在一开始就接收可用的响应。其余的可以安全卸载。真正唯一的问题是 async/await 是否可以帮助,使情况变得更糟,或者在这种情况下无法使用。我必须承认我对它是什么有不同的想法。
  • 我不同意这项工作可能需要卸载。我是说使用async/await 结合Task.Yield 有难闻的气味。在这里使用ThreadPool.QueueUserWorkItem 会更合适。毕竟,这确实是您想要做的......将工作发送到 ThreadPool 并使用相当少的代码占用空间,对吧?
  • 哦,好的。公平的评论,我误解了你的说法。我想我只是认为使用异步我只会调用一个方法,它会神奇地从另一个线程开始:)。说到不同的方法,有人知道这三者之间的比较吗? ThreadPool.QueueUserWorkItem vs Task.Factory.StartNew vs delegate.BeginInvoke?如果我要做出改变,我不妨以最好的方式去做。
  • 这能回答你的问题吗? Fire and forget async method in asp.net mvc

标签: c# asynchronous c#-5.0


【解决方案1】:

避免使用async void。它在错误处理方面具有棘手的语义;我知道有些人称它为“fire and forget”,但我通常使用“fire and crash”这个短语。

问题是:给定一个同步方法 A(),我怎样才能使用 async/await 以一种即发即弃的方式异步调用它,而不会得到比“旧方法”更复杂的解决方案

您不需要async / await。就这样称呼它:

Task.Run(A);

【讨论】:

  • 如果 A() 中有异步方法调用怎么办?
  • 我的意思是,你如何避免关于不等待任务的警告?
  • 该警告存在,因为async 方法中的即发即弃几乎可以肯定是一个错误。如果您肯定确定这是您想要做的,您可以将结果分配给未使用的局部变量,如下所示:var _ = Task.Run(A);
  • @AnthonyJohnston:我的意思是从async 方法调用一个即发即弃的方法几乎可以肯定是一个错误。在您的情况下,由于您始终在方法中处理异常,因此async Taskasync void 之间几乎没有区别。我仍然会更倾向于async Task,因为async void 对我来说意味着“事件处理程序”。
  • 但是如果你想在 UI 线程而不是线程池上运行方法 A() 怎么办?而且你不能等待 A() 因为它会在 UI 线程上异步运行更长的时间(而不是阻塞)。例如一个点击处理程序,它必须触发并忘记调用 A() 并在之后做一些事情。但否则不关心A()。 A() 只需要启动。
【解决方案2】:

正如其他答案中所述,通过这个出色的blog post,您希望避免在 UI 事件处理程序之外使用async void。如果您想要一个安全“即发即弃”async 方法,请考虑使用此模式(感谢 @ReedCopsey;此方法是他在聊天对话中给我的):

  1. Task 创建一个扩展方法。它运行传递的Task 并捕获/记录任何异常:

    static async void FireAndForget(this Task task)
    {
       try
       {
            await task;
       }
       catch (Exception e)
       {
           // log errors
       }
    }
    
  2. 在创建它们时始终使用Task 样式async 方法,切勿使用async void

  3. 以这种方式调用这些方法:

    MyTaskAsyncMethod().FireAndForget();
    

您不需要await 它(它也不会生成await 警告)。它还会正确地处理任何错误,因为这是你唯一放置async void的地方,所以你不必记得在任何地方都放置try/catch块。

这也为您提供了选项,如果您真的想正常使用await,则将async 方法用作“即发即弃”方法。

【讨论】:

  • 好吧,如果我有一个任务,我就运行它,不是吗?
  • @mmix 这取决于,您可以使用 Task 对象并运行它,但那不是使用等待/异步。这就是您使用 await/async 执行“触发并忘记”的方式。请注意,当您调用异步框架方法并且您希望以“即发即弃”的方式使用它们时,这更多非常有用。
  • 嗨,这是一篇旧帖子,但通常的想法是使用语言“流”元素来实现即发即弃,而不是隐式使用 Task 对象。我们得出的结论是这是不可能的,因为调用 async 在等待之前不会引发新线程。如果我有 Task 对象,那么我只需 Run()-it,它就会触发并忘记。
  • @mmix 没问题,这只是在我与 Reed Copsey 的一次讨论中提出的,在另一个问题中,我们讨论了关于使用 async void 进行“一劳永逸”的讨论指出这个问题,为什么不这样做。我将其添加为利用 async void 执行此操作的“正确”方式。
  • @Wellspring 由于 ASP.NET 如何管理请求后对象的生命周期,有整个库来管理它(如 Hangfire)。我不建议在这种情况下只发送任务
【解决方案3】:

在我看来,“等待”和“一劳永逸”是两个正交的概念。您要么异步启动一个方法并且不关心结果,要么您希望在操作完成后继续在原始上下文上执行(并且可能使用返回值),这正是 await 所做的。如果你只想在 ThreadPool 线程上执行一个方法(这样你的 UI 就不会被阻塞),去

Task.Factory.StartNew(() => DoIt2("Test2"))

你会没事的。

【讨论】:

  • 我对它进行的实验越多,它看起来就越是如此。 async 仅适用于您对异步任务的结果有有意义的延续的进程。无需继续,无需支持(Task.Yield() 除外)。我想我又被营销狙击了……
  • 在主题上,delegate.BeginInvokeTask.Factory.StartNew 之间有什么真正的区别吗?
  • @mmix,使用Task最大的不同是,如果Task中出现异常,最终会在Task对象的finalizer中被抛出,因为没有观察到异常状态任务。如果您不注册 TaskScheduler.UnobservedTaskException 事件,这可能会导致严重的崩溃,而不会触发您通常的最后手段日志记录方法。它还有一个不幸的副作用,即在 GC 导致终结器运行之前不会崩溃,而调用的委托将在异常发生后立即使应用程序崩溃。
  • @DanBryant:This has changed in .NET 4.5UnobservedTaskException 将不再使进程崩溃;如果你不处理它,异常会被忽略。
  • 一开始我也有同样的感觉;我花了 很长 时间来欣赏这个设计。未来基于Task的代码将基于async;在这个新世界中,一个未被观察到的Task一个一劳永逸的Task。与旧行为一样,这并没有违反快速失败的哲学。默认情况下,旧行为会崩溃,因为之前的某个不确定时间发生了一些错误,因此旧行为无论如何都不是“快速失败”。
【解决方案4】:

我的感觉是,这些“一劳永逸”的方法在很大程度上是需要一种干净的方式来交错 UI 和后台代码的人工制品,以便您仍然可以将逻辑编写为一系列顺序指令。由于 async/await 负责通过 SynchronizationContext 进行编组,因此这不再是一个问题。较长序列中的内联代码有效地成为您的“即发即弃”块,这些块以前会从后台线程中的例程中启动。这实际上是模式的反转。

主要区别在于等待之间的块更类似于 Invoke 而不是 BeginInvoke。如果您需要更像 BeginInvoke 的行为,您可以调用下一个异步方法(返回一个任务),然后在您想要“BeginInvoke”的代码之后才真正等待返回的任务。

    public async void Method()
    {
        //Do UI stuff
        await SomeTaskAsync();
        //Do more UI stuff (as if called via Invoke from a thread)
        var nextTask = NextTaskAsync();
        //Do UI stuff while task is running (as if called via BeginInvoke from a thread)
        await nextTask;
    }

【讨论】:

  • 实际上我们使用 F&F 来避免阻塞 http 调用者,它更多地与调用者限制有关,而不是我们自己的。逻辑是合理的,因为调用者不希望收到除收到消息之外的响应(实际的进程响应将发布在与此无关的另一个通道上,或者与此无关的 http)。
【解决方案5】:

这是我根据 Ben Adams 关于构建此类构造的推文整理的课程。 HTHhttps://twitter.com/ben_a_adams/status/1045060828700037125

using Microsoft.Extensions.Logging;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

// ReSharper disable CheckNamespace
namespace System.Threading.Tasks
{
    public static class TaskExtensions
    {
        [SuppressMessage("ReSharper", "VariableHidesOuterVariable", Justification = "Pass params explicitly to async local function or it will allocate to pass them")]
        public static void Forget(this Task task, ILogger logger = null, [CallerMemberName] string callingMethodName = "")
        {
            if (task == null) throw new ArgumentNullException(nameof(task));

            // Allocate the async/await state machine only when needed for performance reasons.
            // More info about the state machine: https://blogs.msdn.microsoft.com/seteplia/2017/11/30/dissecting-the-async-methods-in-c/?WT.mc_id=DT-MVP-5003978
            // Pass params explicitly to async local function or it will allocate to pass them
            static async Task ForgetAwaited(Task task, ILogger logger = null, string callingMethodName = "")
            {
                try
                {
                    await task;
                }
                catch (TaskCanceledException tce)
                {
                    // log a message if we were given a logger to use
                    logger?.LogError(tce, $"Fire and forget task was canceled for calling method: {callingMethodName}");
                }
                catch (Exception e)
                {
                    // log a message if we were given a logger to use
                    logger?.LogError(e, $"Fire and forget task failed for calling method: {callingMethodName}");
                }
            }

            // note: this code is inspired by a tweet from Ben Adams: https://twitter.com/ben_a_adams/status/1045060828700037125
            // Only care about tasks that may fault (not completed) or are faulted,
            // so fast-path for SuccessfullyCompleted and Canceled tasks.
            if (!task.IsCanceled && (!task.IsCompleted || task.IsFaulted))
            {
                // use "_" (Discard operation) to remove the warning IDE0058: Because this call is not awaited, execution of the
                // current method continues before the call is completed - https://docs.microsoft.com/en-us/dotnet/csharp/discards#a-standalone-discard
                _ = ForgetAwaited(task, logger, callingMethodName);
            }
        }
    }
}

【讨论】:

    猜你喜欢
    • 2017-06-23
    • 2012-07-13
    • 2010-11-27
    • 1970-01-01
    • 2016-07-20
    • 2017-06-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多