使用异步vs“旧异步委托”进行即发即弃

时间:2012-10-09 15:04:31

标签: c# asynchronous c#-5.0

我正在尝试用新语法替换旧的“即发即弃”调用,希望更简单,似乎在逃避我。这是一个例子

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和关闭句柄,这仍然有点争议)我正在以新的方式失去等待一个Task.Yield(),这实际上提出了一个新问题,即必须重写所有现有的异步F&amp; F方法才能添加一个单行。在性能/清理方面是否有一些无形的收获?

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

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

5 个答案:

答案 0 :(得分:76)

避免使用async void。它有关于错误处理的棘手语义;我知道有些人称之为“火与忘记”,但我通常会使用“火灾和崩溃”一词。

  

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

您不需要async / await。只需将其称为:

Task.Run(A);

答案 1 :(得分:46)

正如其他答案所述,以及优秀的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();
    
  4. 您不需要await它(也不会生成await警告)。它还会处理正确的任何错误,因为这是您放置async void的唯一地方,您不必记得在任何地方放置try/catch块。

    如果您确实想要async正常,这也为您提供选项,使用await方法作为“即发即弃”方法。

答案 2 :(得分:18)

对我而言,似乎“等待”某些东西并“发射并忘记”是两个正交的概念。您可以异步启动方法而不关心结果,或者您希望在操作完成后继续在原始上下文上执行(并且可能使用返回值),这正是await所做的。如果你只想在ThreadPool线程上执行一个方法(这样你的UI就不会被阻止),那就去找

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

你会没事的。

答案 3 :(得分:1)

我的感觉是,这些“即发即忘”的方法主要是需要一种干净的方式来交错UI和背景代码的工件,这样你仍然可以将逻辑编写为一系列顺序指令。由于async / await负责通过SynchronizationContext进行编组,因此这不再是一个问题。较长序列中的内联代码有效地成为以前从后台线程中的例程启动的“即发即忘”块。它实际上是对模式的反转。

主要区别在于awaits之间的块更类似于Invoke而不是BeginInvoke。如果你需要更像BeginInvoke的行为,你可以调用下一个异步方法(返回一个Task),然后实际上等待返回的Task直到你想要'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;
    }

答案 4 :(得分:0)

这是我根据 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);
            }
        }
    }
}