Fire and Forget方法

42

这个答案有关,

如果我真的想要“发射并忘记”一个返回任务的方法,并且(为简单起见)假设该方法不会抛出任何异常。我可以使用答案中列出的扩展方法:

public static void Forget(this Task task)
{
}

使用这种方法,如果Task的动作中存在导致异常抛出的错误,则当抛出意外异常时,异常将被吞噬并不会被注意到。

问题:在这种情况下,扩展方法是否更适合采用以下形式:

public static async void Forget(this Task task)
{
    await task;
}

编程错误会抛出异常并升级(通常导致进程崩溃)。

在存在预期(且可忽略)异常的方法中,该方法需要变得更为复杂(顺便问一下,如何构建一个版本的该方法,以接受可接受和可忽略异常类型的列表?)


关于如何忽略任务异常的想法,请参考以下链接:在等待任务时忽略特定类型异常的更简单方法 - Theodor Zoulias
3个回答

54

这取决于你想要的语义。如果你想确保异常被注意到,那么是的,你可以await这个任务。但在这种情况下,它并不是真正的“发射和忘记”。

真正的“发射和忘记”——也就是说,你不关心它何时完成,以及它是否成功完成或出现错误——非常罕见。

编辑:

处理异常:

public static async void Forget(this Task task, params Type[] acceptableExceptions)
{
  try
  {
    await task.ConfigureAwait(false);
  }
  catch (Exception ex)
  {
    // TODO: consider whether derived types are also acceptable.
    if (!acceptableExceptions.Contains(ex.GetType()))
      throw;
  }
}

请注意,我建议使用await而不是ContinueWith。如我在博客中所述,ContinueWith的默认计划程序令人惊讶,并且Task.Exception会将实际异常包装在AggregateException中,使错误处理代码更加繁琐。


4
那么,我该如何表达不关心某些事情何时结束的语义,但我关心异常呢?是否可能在某种程度上不使用 async void 实现这些语义? - Matt Smith
2
你是不是想太多了,还是我没理解到位?听起来你需要一个 try/catch。下面是示例代码: - Stephen Cleary
1
@Noseratio 和 Stephen,Stephen 你的编辑看起来正是我想要的。谢谢。是的,就像 Noseratio 所说,我计划将其用作 fire-and-forget 的通用存根(除了不要“忘记”/吞噬异常)。 - Matt Smith
5
catch块不会在捕获的上下文中运行(因此任何日志记录或其他操作都不会执行),但如果异常是不可接受的并且被重新抛出(throw;),那么异常将在 Forget 开始时处于活动状态的 SynchronizationContext 上(重新)抛出。 - Stephen Cleary
3
它实际上是调用了 Post 方法。 - Stephen Cleary
显示剩余5条评论

4
在这个相关问题中,我最初想要在下面的场景中使用static void Forget(this Task task)
var task = DoWorkAsync();
QueueAsync(task).Forget();

// ...

async Task QueueAsync(Task task)
{
    // keep failed/cancelled tasks in the list
    // they will be observed outside
    _pendingTasks.Add(task);
    await task;
    _pendingTasks.Remove(tasks)
}

看起来很棒,但后来我意识到由_pendingTasks.Add / _pendingTasks.Remove可能引发的致命异常会无法被观察到和捕获,这是不好的。请参考fatal exceptions
所以我简单地将QueueTask变成了一个async void方法,它本质上就是这样的。
var task = DoWorkAsync();
QueueAsync(task);

// ...

async void QueueAsync(Task task)
{
    // keep failed/cancelled tasks in the list
    // they will be observed outside
    _pendingTasks.Add(task);
    try
    {
        await task;
    }
    catch
    {
        return;
    }
    _pendingTasks.Remove(tasks)
}

虽然我不喜欢空 catch {},但我认为在这里使用它是有道理的。

在这种情况下,保留async Task QueueAsync()并像你提议的那样使用async void Forget(this Task task)会过度设计,我个人认为。


3

如果你对任务是否抛出异常感兴趣,那么你需要await结果,但相反的是,这基本上违背了“fire & forget”的目的。

在想要知道是否发生了一些不好的事情的情况下,推荐的方法是使用continuation,例如:

public static void ForgetOrThrow(this Task task)
{
    task.ContinueWith((t) => {
        Console.WriteLine(t.Exception);
    }, TaskContinuationOptions.OnlyOnFaulted);
}

如果您将“忘记”方法变成异步方法,那么您基本上重新引入了相同的问题。这是什么意思 - 它确实抑制了警告,并且在没有抛出异常的情况下具有相同的行为。我想我需要“Fire and Forget”,但我不想忽略错误。 - Matt Smith
@MattSmith 它抑制了警告吗?警告是由于在async方法内部没有对WorkAsync()进行await调用而导致的。Forget扩展方法(什么也不做)抑制了警告,因为它是一个非异步方法。再次将其设置为async应该会重新引入警告,因为您又回到了调用异步方法而不是调用await(除非扩展方法被单独处理...)。 - James
3
它是“async void”,因此没有任何可等待的内容。 - Matt Smith
1
Albahari在《C# 5.0权威指南》的第23章中提出了同样的想法,作为“吞下”任务未处理异常的一种方式,但指出您可以对异常进行一些操作,例如记录日志...因此我想它可以扩展到您忽略某些异常并重新抛出其他异常的想法。不仅仅是throw t.Exception,您可以先“观察”它(var ignore = t.Exception;),如果不是可接受的异常,然后再抛出它:if(reallyBad) {throw t.Exception;} 这允许简单地忽略可接受的异常。 - mdisibio
@SørenBoisen 在該年對話的時間點上,Stephen在James之後發布了他傑出的答案,因此他的答案優於James的答案和我的評論。 也就是說,(我遠不及Cleary或這個頁面上的任何其他人那麼專業),我相信通過觀察先行例外,它不再被認為是“未處理的”。 引用Albahari的話:“一種安全的模式是重新拋出先行例外。 只要繼續等待,異常就會傳播並重新拋出到等待者。”(第942頁) - mdisibio
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接