如何确保操作不会导致整个应用程序崩溃?

4

我有一个应用程序,会执行一些额外的任务,比如清理旧日志、发送通知等。如果其中一个任务失败了,我不希望整个应用程序停止工作并且不再执行剩下的任务。

例如:

await SendUsersBirthdayEmailsAsync(); // <-- if something fails while trying to send birthday emails here, I don't want the app to stop working and not clean logs and so on...
await DeleteOutdatedLogsAsync();
await SendSystemNotificationsAsync();

你会推荐我选哪个?


9
为什么不使用try-catch... - 42LeapsOfFaith
这是什么类型的应用程序(控制台/ wpf / winforms / ...)?“崩溃整个应用程序”是什么意思?如果生日邮件逻辑出现故障,您想要做什么? - Fortega
1
@42LeapsOfFaith 你的意思是只捕获一般的异常,比如 try { ... } catch { ... } 吗? - kseen
@Fortega 这是一个控制台应用程序,由Windows任务计划程序每晚运行。如果生日邮件发送失败,我只想记录下来,然后让应用程序继续完成其余的任务。 - kseen
4
问题在于,如果你不知道可能会出现哪些故障,你就没有办法知道程序是否仍然适合执行任何进一步的操作。不要试图“不管发生什么”都继续前进。捕获/处理您可以合理预期和处理的错误,否则最明智的做法是允许程序崩溃并停止运行。除非您正在编写安全关键系统,在这种情况下您将不会使用C#。 - Damien_The_Unbeliever
显示剩余2条评论
11个回答

8

在代码的每个可能失败的部分使用try-catch块。
根据需要使用try-catch-finally块。

在每个catch块中,按照自己的需求记录异常。我使用Nlog进行记录,建议查看该工具。

try{
    //do work here
}
catch(Exception e){
    //log exception here
}
//optional
finally{
    //do optional needed work here
}

类似这样:

public bool SendUsersBirthdayEmailsAsync(){
    try{
        SendMail();
    }
    catch(Exception e){
        LogException(e);
    }
    //optional
    finally{
        OptionalWork();
    }       
}

编辑:关于避免使用通用异常

您总是可以使用多个catch块来为每种类型的异常定义不同的行为。当您知道可以预期哪种异常时,这非常有用。
例如:

public bool SendUsersBirthdayEmailsAsync(){
    try{
        SendMail();
    }
    catch (ThreadAbortException tae)
    {
        LogException(tae);
        //do something specific
    }
    catch (ThreadInterruptedException tie)
    {
        LogException(tie);
        //do something specific
    }
    catch(Exception e){
        LogException(e);
    }
    //optional
    finally{
        OptionalWork();
    }       
}

EDIT 2: 异常处理的官方Microsoft指南请参考这里
使用try/catch块包围可能会生成异常并且您的代码可以从该异常中恢复的代码。在catch块中,始终将异常从最派生到最少派生进行排序。所有异常都派生自Exception。较派生的异常不会被先于基本异常类的catch子句处理。当您的代码无法从异常中恢复时,请不要捕获该异常。如有可能,请启用调用堆栈上更高层次的方法进行恢复。
清理使用using语句或finally块分配的资源。建议使用using语句在抛出异常时自动清理资源。使用finally块清理不实现IDisposable的资源。finally子句中的代码几乎总是在抛出异常时执行。

有没有可能避免使用通用异常处理,同时尽可能实现最容错?我已经在这里询问了你的做法:https://stackoverflow.com/questions/57588098/is-general-exception-handling-not-so-bad-in-this-case - kseen
这取决于您正在执行的操作。例如,如果您正在使用线程,则可以使用catch(ThreadInterruptedException)。使用通用异常处理是程序不崩溃且能够恢复的最安全方式。 - Matt
你可以尝试在不同的线程上启动每个任务,并使用原始线程作为看门狗。 - 42LeapsOfFaith
@kseen 请检查我的回答编辑,如果那是你的意思。 - Matt
另外,请查看我在答案的Edit2中刚刚放置的链接,也许它可以帮助你找到一些有用的东西 :) - Matt
显示剩余3条评论

0

Andreas使用的答案符合我的想法,但我会再加一个部分——带有时间限制的取消令牌,以确保所有任务都能在规定时间内完成。

在Wait中,各个任务在执行时是独立的,但它们会在这一点上汇聚。如果出现异常,你可以捕获聚合异常,然后确定是超时还是需要记录日志。

UnhandledException和UnhandledTask Exception的两个建议也应该包括在内,无论出于何种原因停止。

我还考虑将其作为Windows服务运行,这样你就可以从启动时将取消令牌传递到任务中,并设置一个计时器来确定服务实际运行的时间,而不是使用计划任务。通过所有的异常处理,你可以随着时间的推移查看日志告诉你的内容,这对我来说感觉比控制台应用程序更具弹性,但这是我们通常在这种情况下运行任务的方式。

您可以在控制台应用程序中启动和停止服务进行测试,然后将其移交给处理OnStart和OnStop事件的项目 - 传递Cancellation token - OnStop会取消token并停止服务循环。

0

如果您已经使用调度程序进行操作,为什么不将其分解为更小的任务呢?这样,如果一个操作失败了,它就不会影响其他所有操作。此外,拥有许多具有专门职责的较小应用程序而不是执行所有操作的通用应用程序是一种良好的实践。


0

如果你正在寻找一个可靠的选择来确保进程完成并且可以追踪,那么可以选择Hangfire。你也可以通过处理异常来放置重试逻辑以应对失败情况。


0

您可以使用Try-Pattern实现您的方法,并使它们返回布尔值。

让每个方法自己处理异常,以确保没有未处理的异常会导致应用程序崩溃。

如果失败将导致不可恢复的程序状态,则方法应返回false,并且您的应用程序应自行以清洁的方式退出。


0
Task<Task> task = (SendUsersBirthdayEmailsAsync()
            .ContinueWith(x => Task.WhenAll(DeleteOutdatedLogsAsync(), SendSystemNotificationsAsync())));
await await task;

如需更一般化的示例,请参考以下代码:

class TaskTest
{
    public async void Start()
    {

        await (
            (await One().ContinueWith(x => Task.WhenAll(Two(), Three())))
            .ContinueWith(x=> Four()));
    }

    private async Task One()
    {
        await Task.Delay(5000);
        Console.WriteLine("1");
        throw new Exception();
    }

    private async Task Two()
    {
        await Task.Delay(2000);
        Console.WriteLine("2");
        throw new Exception();

    }
    private async Task Three()
    {
        await Task.Delay(3000);
        Console.WriteLine("3");
        throw new Exception();
    }

    private async Task Four()
    {
        await Task.Delay(1000);
        Console.WriteLine("4");
        throw new Exception();
    }
}

运行这段代码会显示在任务中抛出异常并不会停止整个程序。

0

根据方法名称的暗示,这些方法是相互独立的,只需确保每个方法返回一个可等待的任务并将它们放入列表中。您使用的await表明它们已经这样做了。

在其周围放置一个try-catch,并捕获AggregateExeption异常。

List<Task> tasks = new List<Task>();
try
{
  tasks.Add(SendUsersBirthdayEmailsAsync());
  tasks.Add(DeleteOutdatedLogsAsync());
  tasks.Add(SendSystemNotificationsAsync());

  Task.WhenAll(tasks); // waits for all tasks to finish
}
catch (Exception e)
{
   //Log e.ToString(); will give you all inner exceptions of the aggregate exception as well, incl. StackTraces, so expect possibly a lot of chars, wherever you log it.
}

这样做可能会加快速度,因为它们现在是并行运行的。但由于它们似乎都在处理一个数据库(很可能是同一个),所以由于调度开销,可能不会有太大的提升。


0

未观察到的任务异常

await SendUsersBirthdayEmailsAsync(); // uses TaskScheduler

由于您正在使用TaskScheduler,请查看TaskScheduler.UnobservedTaskException

当故障任务的未观察异常即将触发异常升级策略时,会发生此情况,默认情况下,这将终止进程。


0

你需要在每个函数中使用以下的Try-Catch。

try{
    //write your code
}
catch(Exception e){
    //your exception will be here
}

您可以为该异常生成日志文件。只需为日志文件执行一项操作。创建一个日志类和一个日志文件生成函数。在所有函数的catch区域中调用该函数。

让您创建一个名为clsLog的日志类,并创建一个名为InsertLog(string exception, string functionname)的静态函数。

在所有函数中使用此日志方法,如下所示。

public void insertcity()
{
 try
 {
   //Insert city programming
 }
 catch (exception ex)
 {
   clsLog.InsertLog(ex.Message,"insertCity");
   //do something else you want.
 }        
}

希望这能有所帮助。


0

我认为如果你使用

try{
    //your nice work
}
catch(Exception e){
    //hmm.. error ok i will show u
}
//optional
finally{
    //again start the operation if u failed here.
}

我也制作了同样的应用程序,当出现错误并失败时,在捕获异常中将其记录在系统中,并在最终重新启动应用程序。这样它就永远不会死掉。

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