在另一个线程上捕获异常?

时间:2014-02-19 15:36:13

标签: c# multithreading winforms invoke synchronizationcontext

在开发winform应用程序时,通常只需调用以获取主GUI线程来完成GUI工作。

Invoke今天已过时(如果我读的正确),而是假设您使用SynchronizationContext代替。

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

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

2 个答案:

答案 0 :(得分:6)

首先,同步上下文并不是什么新东西,它自.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());
        }
    });
}

答案 1 :(得分:0)

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线程中要处理的方法。 / p>

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

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