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

Posted

技术标签:

【中文标题】如何确保操作不会使整个应用程序崩溃?【英文标题】:How do I ensure an operation won't crash the whole app? 【发布时间】:2019-12-26 16:31:21 【问题描述】:

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

例如,

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();

你会推荐我一起去吗?

【问题讨论】:

为什么不用try catch... 这是什么应用程序(控制台/wpf/winforms/...)?您所说的“使整个应用程序崩溃”是什么意思?如果生日邮件逻辑出现问题,你会怎么做? @42LeapsOfFaith 你的意思是要捕获像try ... catch ... 这样的一般异常吗? @Fortega 这是一个由 Windows 任务计划程序每晚运行的控制台应用程序。如果生日邮件失败了,我只想记录它,然后让应用程序继续完成剩下的任务。 问题是,当您不知道可能会发生什么故障时,您无法知道程序是否仍处于适合执行任何进一步操作的状态 .不要试图在“无论如何”上当兵。捕获/处理您可以合理预期处理的错误,否则最明智的做法是让程序崩溃和烧毁。除非您正在编写一个安全关键系统,否则无论如何您都不会使用 C#。 【参考方案1】:

在可能失败的代码的每个部分使用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();
           

编辑 2:Official Microsoft guidance 用于异常处理。

在可能产生异常的代码周围使用try/catch 块,并且您的代码可以从该异常中恢复。在catch 块中,始终将异常从最衍生到最小的顺序排列。所有异常都源自Exception。更多派生异常不会由 catch 子句处理,该子句前面是基异常类的 catch 子句。当您的代码无法从异常中恢复时,不要捕获该异常。如果可能,使调用堆栈更靠前的方法能够恢复。

清理使用using 语句或finally 块分配的资源。首选using 语句在抛出异常时自动清理资源。使用finally 块来清理没有实现IDisposable 的资源。即使抛出异常,finally 子句中的代码也几乎总是被执行。

【讨论】:

我们是否有机会避免使用通用异常处理并尝试使其具有最大的容错性?我问过你在这里的做法:***.com/questions/57588098/… 这取决于您正在执行的操作。例如,如果您使用线程,则可以使用 catch (ThreadInterruptedException)。使用通用异常处理是程序不爆炸并能够恢复的最安全方法。 您可以尝试在不同的线程上启动每个任务,并将原始线程用作看门狗。 @kseen 请检查我在答案中的编辑,如果那是你的意思。 @Matt 感谢您的澄清。但是不,在这种情况下,我不知道究竟会发生什么异常。有没有什么方法可以不使用通用异常处理但又保证它的安全性? :)【参考方案2】:
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();
    

运行此代码表明在任务中抛出异常不会停止整个程序。

【讨论】:

【参考方案3】:

如果您已经在使用调度程序执行此操作,为什么不将其分成更小的任务呢?这样,如果一个动作失败,它不会降低所有其他动作。此外,拥有许多具有专门职责的小型应用程序而不是一个通用的应用程序是一种很好的做法。

【讨论】:

【参考方案4】:

如果您正在寻找一个可靠的选项来确保流程已完成且可追溯,您可以选择Hangfire。您还可以通过处理异常来放置重试逻辑以防失败。

【讨论】:

【参考方案5】:

由于方法的名称表明这些方法是相互独立的,只需确保每个方法都返回一个可等待的任务并将它们放入一个列表中。您对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.

这也可能会加快速度,因为它们现在是并行运行的。但由于它们似乎都在数据库上工作(很可能是相同的),由于调度开销,它可能不会太多,如果有的话。

【讨论】:

【参考方案6】:

您可以使用Try-Pattern 实现您的方法并让它们返回布尔值。 让每个方法自己处理异常,这样您就可以确定不会抛出未处理的异常来杀死您的应用程序。

如果失败会导致程序状态不可恢复,这些方法应返回 false,并且您的应用程序应自行以干净的方式退出。

【讨论】:

【参考方案7】:

未观察到的任务异常

await SendUsersBirthdayEmailsAsync(); // uses TaskScheduler

由于您使用的是 TaskScheduler,请查看 TaskScheduler.UnobservedTaskException。

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

【讨论】:

【参考方案8】:

您需要在每个函数中使用 Try-Catch,如下所示。

try
    //write your code

catch(Exception e)
    //your exception will be here

您可以为该异常生成一个日志文件。 只需为您的日志文件做一件事。创建一个日志类和一个日志文件生成函数。 在所有函数的捕获区域中调用该函数。

让您创建一个名为 clsLog 的 Log 类。和一个名为 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.
         

希望它会有所帮助。

【讨论】:

【参考方案9】:

我想如果你使用

try
    //your nice work

catch(Exception e)
    //hmm.. error ok i will show u

//optional
finally
    //again start the operation if u failed here.

我还制作了相同的应用程序,其中当出现一些错误并失败时,我会在 catch 异常中登录系统,并在 finally 中再次重新启动应用程序。所以它永远不会死。

【讨论】:

【参考方案10】:

我建议找出您从 async 获得的异常以实际获取此异常并了解您可以在主要 Thread 上使用它:

MyTask().GetAwaiter().GetResult();

这将允许您查看您在 await 线程上的实际异常的文本。


如果您不希望主线程返回到 await 的行,您也可以使用:

await SendUsersBirthdayEmailsAsync().ConfigureAwait(false);

这只会在最近的空闲线程上继续您的任务。

但是不得不说这会在android上引发异常,因为它在使用Threads上有一些限制。


如果您不想浪费时间或不需要手动处理异常并修复它try, catch finally 始终是一个选项。

【讨论】:

【参考方案11】:

Andreas 使用的 answer - 符合我的想法 - 但我会再制作一件 - 取消令牌 - 有时间限制,您认为时间足够长 - 以便一切完成。

Wait 中的各个任务意味着它们在各自执行时都在该点一起返回。如果发生聚合门异常,您可以捕获它们 - 然后确定它是否超时 - 或者您需要记录的内容。

还应该包括关于 UnhandledException 和 UnhandledTask Exception 的两条建议 - 无论出于何种原因,它们都会停止。

我还会考虑将其设为 Windows 服务,以便您可以从启动时将取消令牌传递给任务 - 并在服务实际运行时设置计时器 - 而不是计划任务 - 然后进行所有异常处理随着时间的推移,您可以看到日志告诉您的内容——对我来说,这比控制台应用程序更有弹性——但这就是我们通常以这种方式运行任务的方式。

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

【讨论】:

以上是关于如何确保操作不会使整个应用程序崩溃?的主要内容,如果未能解决你的问题,请参考以下文章

如何阻止一个节点上的死锁使整个集群崩溃?

按钮操作使应用程序崩溃[重复]

如何确保代码不会被多次调用,但不会像 dispatch_once 那样在整个程序执行中阻塞它?

按钮单击使整个应用程序崩溃

如何使用 IndexedDB 制作一个非常长的字符串而不会使浏览器崩溃?

防止来自 3rd 方组件的异常使整个应用程序崩溃