以下代码说明了我的困境。代码创建一个后台线程来处理某些东西,然后用结果调用UI线程。
如果后台线程在表单关闭后调用窗体上的Invoke,则可能会抛出异常。它在调用Invoke之前检查IsHandleCreated,但是在检查后表单可能会关闭。
void MyMethod()
{
// Define background thread
Action action = new Action(
() =>
{
// Process something
var data = BackgroundProcess();
// Try to ensure the form still exists and hope
// that doesn't change before Invoke is called
if (!IsHandleCreated)
return;
// Send data to UI thread for processing
Invoke(new MethodInvoker(
() =>
{
UpdateUI(data);
}));
});
// Queue background thread for execution
action.BeginInvoke();
}
一种解决方案可能是同步FormClosing和每次调用Invoke,但这听起来不是很优雅。有更简单的方法吗?
答案 0 :(得分:5)
是的,这里有一场比赛。在目标开始运行之前,A需要很长的时间。如果使用Control.BeginInvoke(),它将更好地工作,表单的Dispose()实现将清空调度队列。但这仍然是一场比赛,尽管很少会发生。您在代码段中编写的代码不需要Invoke()。
唯一干净的解决方法是将FormClosing事件和延迟关闭联锁,直到您确认后台线程已完成且无法再次启动。您的代码不容易,因为这需要“完成”回调,因此您可以真正关闭表单。 BackgroundWorker would be a better mousetrap。 Q& D修复是捕获BeginInvoke将引发的ObjectDisposedException。鉴于使用BeginInvoke()时这种情况有多么罕见,这种丑陋的黑客行为是可以接受的。你无法测试它:)
答案 1 :(得分:2)
我通过使用Hans Passant建议来捕获ObjectDisposedException来解决BeginInvoke的这个同步问题。到目前为止,它似乎有效。我创建了Control类的扩展方法来实现这一点。
TryBeginInvoke尝试在控件上调用自己的方法。如果成功调用该方法,它将检查控件是否已被释放。如果已经处理,它会立即返回;否则,它将最初作为参数传递给TryBeginInvoke的方法调用。代码如下:
public static class ControlExtension
{
// --- Static Fields ---
static bool _fieldsInitialized = false;
static InvokeDelegateDelegate _methodInvokeDelegate; // Initialized lazily to reduce application startup overhead [see method: InitStaticFields]
static InvokeMethodDelegate _methodInvokeMethod; // Initialized lazily to reduce application startup overhead [see method: InitStaticFields]
// --- Public Static Methods ---
public static bool TryBeginInvoke(this Control control, Delegate method, params object[] args)
{
IAsyncResult asyncResult;
return TryBeginInvoke(control, method, out asyncResult, args);
}
/// <remarks>May return true even if the target of the invocation cannot execute due to being disposed during invocation.</remarks>
public static bool TryBeginInvoke(this Control control, Delegate method, out IAsyncResult asyncResult, params object[] args)
{
if (!_fieldsInitialized)
InitStaticFields();
asyncResult = null;
if (!control.IsHandleCreated || control.IsDisposed)
return false;
try
{
control.BeginInvoke(_methodInvokeDelegate, control, method, args);
}
catch (ObjectDisposedException)
{
return false;
}
catch (InvalidOperationException) // Handle not created
{
return false;
}
return true;
}
public static bool TryBeginInvoke(this Control control, MethodInvoker method)
{
IAsyncResult asyncResult;
return TryBeginInvoke(control, method, out asyncResult);
}
/// <remarks>May return true even if the target of the invocation cannot execute due to being disposed during invocation.</remarks>
public static bool TryBeginInvoke(this Control control, MethodInvoker method, out IAsyncResult asyncResult)
{
if (!_fieldsInitialized)
InitStaticFields();
asyncResult = null;
if (!control.IsHandleCreated || control.IsDisposed)
return false;
try
{
control.BeginInvoke(_methodInvokeMethod, control, method);
}
catch (ObjectDisposedException)
{
return false;
}
catch (InvalidOperationException) // Handle not created
{
return false;
}
return true;
}
// --- Private Static Methods ---
private static void InitStaticFields()
{
_methodInvokeDelegate = new InvokeDelegateDelegate(InvokeDelegate);
_methodInvokeMethod = new InvokeMethodDelegate(InvokeMethod);
}
private static object InvokeDelegate(Control control, Delegate method, object[] args)
{
if (!control.IsHandleCreated || control.IsDisposed)
return null;
return method.DynamicInvoke(args);
}
private static void InvokeMethod(Control control, MethodInvoker method)
{
if (!control.IsHandleCreated || control.IsDisposed)
return;
method();
}
// --- Private Nested Types ---
delegate object InvokeDelegateDelegate(Control control, Delegate method, object[] args);
delegate void InvokeMethodDelegate(Control control, MethodInvoker method);
}
答案 2 :(得分:1)
看看WindowsFormsSynchronizationContext
。 Post
方法在UI线程上调用您的UpdateUI
委托,而无需专用窗口;这样您就可以跳过调用IsHandleCreated
和Invoke
。
修改:MSDN在"Multithreaded Programming with the Event-based Asynchronous Pattern"下有一些代码示例。
您可能会发现通过位于AsyncOperationManager
之上的WindowsFormsSynchronizationContext
类进行编程更加容易。反过来,BackgroundWorker
组件构建在AsyncOperationManager
之上。
UI线程被定义为您调用AsyncOperationManager.CreateOperation
的线程;你想在CreateOperation
开始时调用MyMethod
,当你知道你在UI线程上时,并在局部变量中捕获它的返回值。
答案 3 :(得分:0)
您可以在调用之前检查表单(或任何控件)上的IsDisposed。
如果表格在此期间被处理,你还应该在你正在调用的实际方法中检查这个。