使用异步与“旧的异步委托”进行解雇和忘记操作

我试图用一种新的语法来替换我的老式“火与忘”调用,希望能够更加简单,但这似乎让我感到困惑。举个例子

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

如果我不能修改后台方法,我将如何应用异步?在我看来,没有直接的方法,我将不得不创建一个包装器异步方法,将等待任务。跑?

编辑: 我现在发现我可能漏掉了一个真正的问题。问题是: 如果给定一个同步方法 A () ,我怎么能够以一种即刻忘记的方式使用 async/await异步调用它,而不得到比“旧方法”更复杂的解决方案呢

67951 次浏览

我的感觉是,这些“发射和忘记”方法在很大程度上是需要一种干净的方式来交错 UI 和背景代码的工件,这样您仍然可以将逻辑写成一系列顺序指令。因为异步/等待负责通过 SynchronizationContext 进行封送处理,所以这不再是一个问题。一个较长序列的内联代码实际上成为你的“发射和忘记”块,这些块以前是从一个后台线程的例程中启动的。这实际上是模式的倒置。

主要区别在于等待之间的块更类似于 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;
}

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

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

你会没事的。

避免 async void。它在错误处理方面有着复杂的语义; 我知道有些人称它为“火与忘”,但我通常使用短语“火与崩溃”。

问题是: 给定一个同步方法 A () ,我如何使用异步/等待方式异步调用它,而不用获得比“旧方法”更复杂的解决方案呢

你不需要 async/await,只需要这样称呼它:

Task.Run(A);

正如其他答案中提到的,通过这个优秀的 博客文章,您希望避免在 UI 事件处理程序之外使用 async void。如果你想要一个 安全的“火与忘记”async方法,可以考虑使用这个模式(@ReedCopsey; 这个方法是他在一次聊天对话中给我的) :

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

    static async void FireAndForget(this Task task)
    {
    try
    {
    await task;
    }
    catch (Exception e)
    {
    // log errors
    }
    }
    
  2. Always use Task style async methods when creating them, never async void.

  3. Invoke those methods this way:

    MyTaskAsyncMethod().FireAndForget();
    

You don't need to await it (nor will it generate the await warning). It will also handle any errors correctly, and as this is the only place you ever put async void, you don't have to remember to put try/catch blocks everywhere.

This also gives you the option of not using the async method as a "fire and forget" method if you actually want to await it normally.

下面是我根据 Ben Adams 关于构造这样一个结构的推文编写的一个类

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://learn.microsoft.com/en-us/dotnet/csharp/discards#a-standalone-discard
_ = ForgetAwaited(task, logger, callingMethodName);
}
}
}
}