The following code demonstrates my dilemma. The code creates a background thread which processes something, then Invokes the UI thread with the result.
It may throw an exception if the background thread calls Invoke on the form after the form has closed. It checks IsHandleCreated before calling Invoke, but the form might close after the check.
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();
}
One solution might be to synchronize FormClosing and every call to Invoke, but that doesn't sound very elegant. Is there an easier way?
Yes, there's a race here. A takes a good millisecond before the target starts running. It will work 'better' if you use Control.BeginInvoke() instead, the form's Dispose() implementation will empty the dispatch queue. But that's still a race, albeit that it will strike very rarely. Your code as written in the snippet doesn't require Invoke().
The only clean fix is to interlock the FormClosing event and to delay the close until you got confirmation that the background thread is completed and can't be started again. Not easy to do with your code as is since that requires a 'completed' callback so you can really get the form closed. BackgroundWorker would be a better mousetrap. The Q&D fix is to catch the ObjectDisposedException that BeginInvoke will raise. Given how rare this will be when you use BeginInvoke(), that ugly hack could be acceptable. You just can't test it :)
I solved this synchronization issue for BeginInvoke by using Hans Passant's recommendation to catch the ObjectDisposedException. So far, it appears to work. I created extension methods of the Control class to facilitate this.
TryBeginInvoke attempts to invoke its own method on the control. If the method is successfully invoked, it checks whether the control has been disposed. If it has been disposed, it returns immediately; otherwise, it calls the method originally passed as a parameter to TryBeginInvoke. The code is as follows:
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);
}
Take a look at WindowsFormsSynchronizationContext. The Post method posts call to your UpdateUI delegate on the UI thread without needing a dedicated window; this lets you skip calling IsHandleCreated and Invoke.
Edit: MSDN has some code examples under "Multithreaded Programming with the Event-based Asynchronous Pattern".
You might find it easier to program via the AsyncOperationManager class, which sits on top of WindowsFormsSynchronizationContext. In turn, the BackgroundWorker component is built on top of AsyncOperationManager.
The UI thread is defined as the one on which you call AsyncOperationManager.CreateOperation; you want to call CreateOperation at the start of MyMethod, when you know you're on the UI thread, and capture its return value in a local variable.
You can check IsDisposed on the form (or any control) before Invoking on it.
You should also check this inside of the actual method you're Invoking, in case the form was disposed in the meantime.
来源:https://stackoverflow.com/questions/3565551/c-sharp-winforms-threading-closed-form-gets-invoked