【问题标题】:Catch exceptions on another thread?在另一个线程上捕获异常?
【发布时间】:2014-02-19 15:36:13
【问题描述】:

在开发 winform 应用程序时,通常只需调用主 GUI 线程即可完成 GUI 工作。

Invoke 今天已经过时了(如果我没看错的话),你应该改用SynchronizationContext

问题是:如何处理异常?我注意到有时在“同步/调用”线程上抛出的异常会丢失?

我确实使用了Application.ThreadExceptionAppDomain.CurrentDomain.UnhandledException,但这没有帮助?

【问题讨论】:

  • 您从哪里得知Invoke 已过时?
  • SynchronizationContext 是一种新奇的方法。我不会说 Invoke 已经过时,但它不再是首选方式。
  • 那么你想用这个异常做什么?当特定后台线程崩溃时,您是否只想通过某种方式关闭整个应用程序?是否要根据另一个线程中存在异常的事实来更新 UI?
  • 我想将异常记录到数据库中。

标签: c# multithreading winforms invoke synchronizationcontext


【解决方案1】:

首先,同步上下文并不是什么新鲜事,它从 .NET 2.0 开始就存在了。它与异常处理无关特别。它也不会使Control.Invoke 过时。事实上,WinFormsSynchronizationContext 是 WinForms 的同步上下文实现,它使用 Control.BeginInvoke 表示 PostControl.Invoke 表示 Send 方法。

如何处理异常?我注意到有时例外 在“同步/调用”线程上抛出的丢了?

这里的“有时”背后有一个有据可查的行为。 Control.Invoke 是一个同步调用,它将异常从回调内部传播到调用线程:

int Test()
{
    throw new InvalidOperationException("Surpise from the UI thread!");
}

void Form_Load(object sender, EventArgs e)
{
    // UI thread
    ThreadPool.QueueUserWorkItem(x =>
    {
        // pool thread
        try
        {
            this.Invoke((MethodInvoker)Test);
        }
        catch (Exception ex)
        {
            Debug.Print(ex.Message);
        }
    });
}

使用SynchronizationContext 的好处在于解耦WinForms 细节。这对于可移植库很有意义,它可能被 WinForms、WPF、Windows Phone、Xamarin 或任何其他客户端使用:

// UI thread
var uiSynchronizationContext = System.Threading.SynchronizationContext.Current;
if (uiSynchronizationContext == null)
    throw new NullReferenceException("SynchronizationContext.Current");

ThreadPool.QueueUserWorkItem(x =>
{
    // pool thread
    try
    {
        uiSynchronizationContext.Send(s => Test(), null);
    }
    catch (Exception ex)
    {
        Debug.Print(ex.ToString());
    }
});

因此,使用Control.Invoke(或SynchronizationContext.Send),您可以选择处理调用线程上的异常。根据设计和常识,Control.BeginInvoke(或SynchronizationContext.Post)没有这样的选择。这是因为Control.BeginInvoke 是异步的,它会将回调排队,以便在Application.Run 运行的消息循环的未来迭代中执行。

为了能够处理异步回调引发的异常,您需要实际观察异步操作的完成情况。在 C# 5.0 之前,您可以使用事件或 Task.ContinueWith

使用事件:

class ErrorEventArgs : EventArgs
{
    public Exception Exception { get; set; }
}

event EventHandler<ErrorEventArgs> Error = delegate { };

void Form_Load(object sender, EventArgs e)
{
    this.Error += (sError, eError) =>
        // handle the error on the UI thread
        Debug.Print(eError.Exception.ToString()); 

    ThreadPool.QueueUserWorkItem(x =>
    {
        this.BeginInvoke(new MethodInvoker(() => 
        {
            try
            {
                Test();
            }
            catch (Exception ex)
            {
                // fire the Error event
                this.Error(this, new ErrorEventArgs { Exception = ex });
            }
        }));
    });
}

使用ContinueWith

ThreadPool.QueueUserWorkItem(x =>
{
    var tcs = new TaskCompletionSource<int>();

    uiSynchronizationContext.Post(s => 
    {
        try
        {
            tcs.SetResult(Test());
        }
        catch (Exception ex)
        {
            tcs.SetException(ex);
        }
    }, null);

    // observe the completion,
    // only if there's an error
    tcs.Task.ContinueWith(task =>
    {
        // handle the error on a pool thread
        Debug.Print(task.Exception.ToString());
    }, TaskContinuationOptions.OnlyOnFaulted);

});

最后,在 C# 5.0 中,您可以使用 async/await 并处理异步抛出的异常,与 try/catch 同步调用一样方便:

int Test()
{
    throw new InvalidOperationException("Surpise from the UI thread!");
}

async void Form_Load(object sender, EventArgs e)
{
    // UI thread
    var uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    await Task.Run(async () =>
    {
        // pool thread
        try
        {
            await Task.Factory.StartNew(
                () => Test(), 
                CancellationToken.None,
                TaskCreationOptions.None,
                uiTaskScheduler);
        }
        catch (Exception ex)
        {
            // handle the error on a pool thread
            Debug.Print(ex.ToString());
        }
    });
}

【讨论】:

  • 谢谢!看来你对此很了解!但是,我确实认为当您涉及 ThreadPool.QueueUserWorkItem 和 Task.Factory.StartNew 时,它会变得有点复杂。我所做的只是 uiSynchronizationContext.Send,我从不为此创建单独的线程,我为什么要这样做?
  • 据我所知,您真正要做的就是在 SynchronizationContext 发送 UI 线程的方法中放置一个 try/catch 块,然后以一种或另一种方式处理缓存?所以为了简化它,你真的建议我在由 SynchronizationContext 触发的方法中尝试/缓存,然后在这里处理异常?
  • @Banshee,如果您所做的只是uiSynchronizationContext.Send 并且从不创建线程,那么您为什么需要使用Send?只有从另一个线程调用它才有意义。这就是为什么我使用QueueUserWorkItem 来展示如何从另一个线程调用它。
  • 您始终有两个选择:在Post/Send 回调内部或外部处理异常。我正在展示后一种情况,它涉及更多,特别是对于异步回调。要决定什么适合任何特定情况,请检查:blogs.msdn.com/b/ericlippert/archive/2008/09/10/…
  • 这确实很棒,但是如果在调用线程上或者使用Post/BeginInvoke时没有处理异常,为什么没有触发ThreadException或UnhandledException?
【解决方案2】:

ui 线程没有自动方法来捕获来自不同线程的异常。

1) 在您的 UI 类中创建一个设计为在 UI 线程上运行的方法,例如 HandleExceptionFromThread(Exception ex);

2) 从 ui 线程中获取 SynchronizationContext。您可以通过调用 SynchronizationContext.Current 来获得它。

3) 将在第二个线程上运行的方法需要将 SynchronizationContext 作为参数。您可能需要做一些从对象到 SyncrhonizationContact 的动态转换,但这应该不会太难。

4) 当捕获到异常时,同步调用uiContext.Send(HandleExceptionFromThead, ex),或者异步调用uiContext.Post(HandleExceptionFromThead, ex),将异常发送到UI线程中要处理的方法。

这是我想象的一些示例代码。

public partial class Form1 : Form
{
    .....
    public void HandleExceptionFromThread(Exception ex)
    {
        MessageBox.Show(ex.Message);
    }

    public void ButtonClickToRunThread(object sender, System.EventArgs e)
    {
        var syncContext = SynchronizationContext.Current;
        Task task = new Task((state)=>
        {
            SynchronizationContext uiContext = state as SynchronizationContext;
            try
            {
                ...
            }
            catch(Exception ex)
            {
                uiContext.Post(HandleExceptionFromThread, ex);
            }
        }, syncContext);
        task.Start();
    }
}

【讨论】:

  • 在我的例子中,SynchronizationContext 是在启动时设置的,无论您在应用程序中的任何位置都可以使用它。您的建议是简单的尝试,在 SynchronizationContext.post 指向的方法中捕获,并在捕获简单日志中将异常与所有其他异常一样?
  • 不完全。 Post 发生在 catch 语句中。它用于调用 UI 线程中的方法。它的工作原理与 Invoke/BeginInvoke 相同。如果您只想记录异常,那么您可以在第二个线程本身中进行。如果您想在 UI 线程中处理它并向用户发出通知,那么这是一种简单的方法。
猜你喜欢
  • 2010-09-16
  • 1970-01-01
  • 1970-01-01
  • 2012-11-04
  • 2013-11-15
  • 1970-01-01
  • 2011-06-25
  • 1970-01-01
  • 2015-06-19
相关资源
最近更新 更多