ChatGPT解决这个技术问题 Extra ChatGPT

异步与“旧异步委托”的一劳永逸

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

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&F 方法才能添加该单行。在性能/清理方面是否有一些看不见的收益?

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

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

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

S
Stephen Cleary

避免 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-Thread 而不是线程池上运行方法 A() 怎么办?而且你不能等待 A() 因为它会在 UI 线程上异步运行更长的时间(而不是阻塞)。例如一个点击处理程序,它必须触发并忘记调用 A() 并在之后做一些事情。但否则不关心A()。 A() 只需要启动。
V
Vinod

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

为 Task 创建一个扩展方法。它运行传递的任务并捕获/记录任何异常: static async void FireAndForget(this Task task) { try { await task; } catch (Exception e) { // 记录错误 } } 在创建它们时始终使用 Task 样式的异步方法,从不使用 async void。以这种方式调用这些方法: MyTaskAsyncMethod().FireAndForget();

您不需要await它(它也不会生成 await 警告)。它还将正确处理任何错误,并且由于这是您放置 async void 的唯一位置,因此您不必记住将 try/catch 块放置在任何地方。

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


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

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

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

你会没事的。


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

我的感觉是,这些“一劳永逸”的方法很大程度上是需要一种干净的方式来交错 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)。
D
Dave Black

这是我根据 Ben Adams 关于构建这种结构的推文整理的课程。 HTH https://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);
            }
        }
    }
}

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅