在开发 winform 应用程序时,通常只需调用主 GUI 线程即可完成 GUI 工作。
Invoke
今天已经过时了(如果我没看错的话),你应该SynchronizationContext
改用。
问题是:如何处理异常?我注意到有时在“同步/调用”线程上抛出的异常会丢失?
我确实使用了Application.ThreadException
andAppDomain.CurrentDomain.UnhandledException
但这没有帮助吗?
在开发 winform 应用程序时,通常只需调用主 GUI 线程即可完成 GUI 工作。
Invoke
今天已经过时了(如果我没看错的话),你应该SynchronizationContext
改用。
问题是:如何处理异常?我注意到有时在“同步/调用”线程上抛出的异常会丢失?
我确实使用了Application.ThreadException
andAppDomain.CurrentDomain.UnhandledException
但这没有帮助吗?
首先,同步上下文并不是什么新鲜事,它从 .NET 2.0 开始就存在。它与异常处理无关。它也不会Control.Invoke
过时。事实上,WinFormsSynchronizationContext
作为 WinForms 的同步上下文实现,使用Control.BeginInvoke
forPost
和Control.Invoke
forSend
方法。
我如何处理异常?我注意到有时在“同步/调用”线程上抛出的异常会丢失?
这里的“有时”背后有一个有据可查的行为。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());
}
});
}
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();
}
}