我正在尝试用新的语法替换我旧的即发即弃的调用,希望更简单,它似乎在逃避我。这是一个例子
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
以即发即弃的方式异步调用它,而不会得到比“旧方法”更复杂的解决方案
Thread.Yield
。我认为这段代码滥用了异步等待哲学。如果没有异步 IO,async/await 可能是错误的解决方案。
async/await
与 Task.Yield
结合使用会产生难闻的气味。在这里使用 ThreadPool.QueueUserWorkItem
会更合适。毕竟,这确实是您想要做的......将工作发送到 ThreadPool 并使用相当少的代码占用空间,对吧?
ThreadPool.QueueUserWorkItem
vs Task.Factory.StartNew
vs delegate.BeginInvoke
?如果我要做出改变,我不妨以最好的方式去做。
避免 async void
。它在错误处理方面具有棘手的语义;我知道有些人称它为“fire and forget”,但我通常使用“fire and crash”这个短语。
问题是:给定一个同步方法 A(),我怎样才能以一种即发即弃的方式使用 async/await 异步调用它,而不会得到比“旧方法”更复杂的解决方案
您不需要 async
/ await
。就这样称呼它:
Task.Run(A);
正如其他答案中所述,通过这个出色的 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
方法用作“即发即弃”方法。
Task
对象并运行它,但那不使用等待/异步。这就是您使用 await/async 执行“触发并忘记”的方式。请注意,当您调用 Async 框架方法并且您希望以“即发即弃”的方式使用它们时,这更多非常有用。
async void
进行即发即用的讨论,我被指出这个问题为什么不要做到这一点。我将此添加为利用 async void
执行此操作的“正确”方式。
在我看来,“等待”某事和“一劳永逸”是两个正交的概念。您要么异步启动一个方法并且不关心结果,要么您希望在操作完成后继续在原始上下文上执行(并且可能使用返回值),这正是 await 所做的。如果您只想在 ThreadPool 线程上执行一个方法(这样您的 UI 不会被阻塞),请选择
Task.Factory.StartNew(() => DoIt2("Test2"))
你会没事的。
delegate.BeginInvoke
和 Task.Factory.StartNew
之间有什么真正的区别吗?
UnobservedTaskException
将不再使进程崩溃;如果你不处理它,异常会被默默地忽略。
Task
的代码将基于 async
;在这个新世界中,未观察到的 Task
是一劳永逸的 Task
。与旧行为一样,这并没有违反快速失败的哲学。默认情况下,旧行为会崩溃,因为之前的某个不确定时间发生了一些错误,因此旧行为无论如何都不是“快速失败”。
我的感觉是,这些“一劳永逸”的方法很大程度上是需要一种干净的方式来交错 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;
}
这是我根据 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);
}
}
}
}
async
方法中的即发即弃几乎可以肯定是一个错误。如果您肯定确定这是您想要做的,您可以将结果分配给一个未使用的局部变量,如下所示:var _ = Task.Run(A);
async
方法调用一个即发即弃的方法几乎肯定是一个错误。在您的情况下,由于您始终在方法中处理异常,因此async Task
和async void
之间几乎没有区别。我仍然会更倾向于async Task
,因为async void
对我来说意味着“事件处理程序”。