【问题标题】:Catch exception after thread is done线程完成后捕获异常
【发布时间】:2020-03-11 10:05:19
【问题描述】:

我在单独的线程中运行一些代码,这可能会引发异常(毕竟,代码往往会这样做)。该线程将从主线程 (GUI) 生成,因此最终必须在此处处理异常(例如设置错误消息文本块)。我有两种解决方案,但都不允许在 GUI 线程中直接捕获异常。

注意:我不能使用像 Task BackgroundWorker (至少不是开箱即用)之类的东西,因为我需要能够更改线程的 ApartmentState

这是我想要的:

var thread = new Thread(() =>
{
    // Code which can throw exceptions
});

try 
{
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    MethodThatAwaitsThread(thread);
}
catch
{
    // Exception handling in the GUI thread
}

这不起作用,因为异常永远不会离开线程。我知道它不能随时离开线程,但我可以等待线程结束,然后捕获它。

这是我当前的解决方案,它利用Dispatcher 与 GUI 线程对话:

var thread = new Thread(() => 
{
    try 
    {
        // Code which can throw exceptions
        Dispatcher.Invoke(UpdateGuiAsSuccess);
    }
    catch (Exception ex)
    {
        Dispatcher.Invoke(UpdateGuiAsError);
    }
}

另一种解决方案是将Exception 存储在一个对象中,然后显式检查它。但这会带来人们忘记检查异常的风险:

Exception ex = null;    

var thread = new Thread(() => 
{
    try 
    {
        // Code which can throw exceptions            
    }
    catch (Exception threadEx)
    {
        ex = threadEx;
    }
}

if (ex != null) 
{
    UpdateGuiAsError();
}
else 
{
    UpdateGuiAsSuccess();
}

一旦工作线程死亡,我是否可以在 GUI 线程中重新抛出错误?

【问题讨论】:

标签: c# wpf multithreading


【解决方案1】:

可以Task 与 STA 线程一起使用(我认为这是您想要的)。

为此,您可以编写一些辅助方法来在已设置为 STA 的线程上启动任务:

public static class STATask
{
    /// <summary>
    /// Similar to Task.Run(), except this creates a task that runs on a thread
    /// in an STA apartment rather than Task's MTA apartment.
    /// </summary>
    /// <typeparam name="TResult">The return type of the task.</typeparam>
    /// <param name="function">The work to execute asynchronously.</param>
    /// <returns>A task object that represents the work queued to execute on an STA thread.</returns>

    [NotNull] public static Task<TResult> Run<TResult>([NotNull] Func<TResult> function)
    {
        var tcs = new TaskCompletionSource<TResult>();

        var thread = new Thread(() =>
        {
            try
            {
                tcs.SetResult(function());
            }

            catch (Exception e)
            {
                tcs.SetException(e);
            }
        });

        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();

        return tcs.Task;
    }

    /// <summary>
    /// Similar to Task.Run(), except this creates a task that runs on a thread
    /// in an STA apartment rather than Task's MTA apartment.
    /// </summary>
    /// <param name="action">The work to execute asynchronously.</param>
    /// <returns>A task object that represents the work queued to execute on an STA thread.</returns>

    [NotNull] public static Task Run([NotNull] Action action)
    {
        var tcs = new TaskCompletionSource<object>(); // Return type is irrelevant for an Action.

        var thread = new Thread(() =>
        {
            try
            {
                action();
                tcs.SetResult(null); // Irrelevant.
            }

            catch (Exception e)
            {
                tcs.SetException(e);
            }
        });

        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();

        return tcs.Task;
    }
}

完成后,您可以轻松创建一个 STA 任务,然后使用 .ContinueWith() 处理任务中抛出的异常,或使用 await 捕获异常。

(注意:[NotNull] 来自 Resharper 注释 - 如果您不使用 Resharper,请将其删除。)

【讨论】:

  • 另请参阅此答案:stackoverflow.com/a/16722767/1136211
  • 这看起来很聪明,至少它按预期工作。我真的不明白是什么原因导致该方法在返回之前等待线程完成。是否可以在您的代码中添加一些非常简短的 cmets 来说明这是如何发生的? :-)
  • @Noceo Run() 方法实际上并不等待 - 它们返回一个已经开始的“热”任务,但在返回时可能会或可能不会完成。等待任务完成的是调用者。在Run() 实现中,会启动一个新线程来执行您的操作/函数,然后调用TaskCompletionSource,SetResult() 以指示任务已完成。
  • @Noceo 实际上是对 await SomeAsyncMethod(); 的调用等待任务完成(并重新抛出任何异常)。
【解决方案2】:

ExceptionDispatchInfo 是在不同线程中重新抛出异常的好解决方案,因为它保留了原始堆栈跟踪。

ExceptionDispatchInfo exceptionInfo = null;

var thread = new Thread(() =>
{
    try
    {
        // Code which can throw exceptions
    }
    catch (Exception ex)
    {
        exceptionInfo = ExceptionDispatchInfo.Capture(ex);
    }
});

try 
{
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    MethodThatAwaitsThread(thread);
    exceptionInfo?.Throw();
}
catch
{
    // Exception handling in the GUI thread
}

【讨论】:

  • 这确实是对我的替代解决方案的升级。但它仍然需要手动捕获并重新抛出异常。
猜你喜欢
  • 1970-01-01
  • 2019-12-09
  • 2015-08-31
  • 2015-01-22
  • 1970-01-01
  • 1970-01-01
  • 2010-09-16
  • 2011-05-20
  • 2011-12-15
相关资源
最近更新 更多