c# - example - ¿Por qué se prefiere InvokeRequired sobre WindowsFormsSynchronizationContext?
thread c# windows form (2)
WindowsFormsSynchronizationContext
funciona adjuntándose a un control especial que está vinculado al subproceso donde se crea el contexto.
Asi que
if (foo.InvokeRequired)
{
foo.BeginInvoke(...)
} else {
...
}
Se puede reemplazar con una versión más segura:
context.Post(delegate
{
if (foo.IsDisposed) return;
...
});
Suponiendo que el context
es un WindowsFormsSynchronizationContext
creado en el mismo subproceso de UI que foo
.
Esta versión evita el problema que evoca:
Justo después de que la secuencia de comandos no GUI ejecute foo.InvokeRequired, el estado de foo puede cambiar. Por ejemplo, si cerramos el formulario justo después de foo.InvokeRequired, pero antes de foo.BeginInvoke, la invocación de foo.BeginInvoke dará lugar a InvalidOperationException: Invoke o BeginInvoke no se pueden invocar en un control hasta que se haya creado el identificador de ventana. Esto no sucedería si cerramos el formulario antes de llamar a InvokeRequired, porque sería falso incluso cuando se lo llama desde un subproceso que no es de la GUI.
Tenga cuidado con algunos casos especiales con WindowsFormsSynchronizationContext.Post
si juega con múltiples bucles de mensajes o múltiples subprocesos de UI:
-
WindowsFormsSynchronizationContext.Post
ejecutará el delegado solo si todavía hay un mensaje de bomba en el hilo donde fue creado. Si no hay nada, no se produce ninguna excepción .
También si posteriormente se conecta otro mensaje a la cadena (a través de una segunda llamada aApplication.Run
por ejemplo) el delegado se ejecutará (se debe al hecho de que el sistema mantiene una cola de mensajes por hilo sin ningún conocimiento sobre el hecho de que alguien está bombeando un mensaje desde él o no) -
WindowsFormsSynchronizationContext.Send
lanzaráInvalidAsynchronousStateException
si el hilo al que está vinculado ya no está activo. Pero si el hilo al que está ligado está activo y no ejecuta un bucle de mensaje , no se ejecutará inmediatamente, sino que se colocará en la cola de mensajes y se ejecutará si se ejecutaApplication.Run
nuevamente.
Ninguno de estos casos debe ejecutar código inesperadamente si se IsDisposed
en un control que se elimina automáticamente (como el formulario principal) ya que el delegado saldrá inmediatamente incluso si se ejecuta en un momento inesperado.
El caso peligroso es llamar a WindowsFormsSynchronizationContext.Send
y teniendo en cuenta que se ejecutará el código: puede que no, y ahora hay forma de saber si hizo algo.
Mi conclusión sería que WindowsFormsSynchronizationContext
es una mejor solución siempre que se use correctamente.
Puede crear problemas sublimados en casos complejos pero aplicaciones GUI comunes con un bucle de mensaje que vive tanto tiempo como la aplicación siempre estará bien.
En cualquier momento, el principiante pregunta algo como: ¿Cómo actualizar la GUI de otro hilo en C #? , la respuesta es bastante clara:
if (foo.InvokeRequired)
{
foo.BeginInvoke(...)
} else {
...
}
Pero, ¿es realmente bueno usarlo? Justo después de que la foo.InvokeRequired
no GUI ejecute foo.InvokeRequired
el estado de foo
puede cambiar. Por ejemplo, si foo.InvokeRequired
formulario justo después de foo.InvokeRequired
, pero antes de foo.BeginInvoke
, la invocación de foo.BeginInvoke
dará lugar a InvalidOperationException
: Invoke o BeginInvoke no se pueden invocar en un control hasta que se haya creado el identificador de ventana. Esto no sucedería si InvokeRequired
el formulario antes de llamar a InvokeRequired
, porque sería false
incluso cuando se lo llama desde un subproceso que no es de la GUI.
Otro ejemplo: digamos que foo
es un TextBox
.Si cierra el formulario, y después de eso el hilo no GUI ejecuta foo.InvokeRequired
(que es falso, porque el formulario está cerrado) y foo.AppendText
dará lugar a ObjectDisposedException
.
Por el contrario, en mi opinión, usar WindowsFormsSynchronizationContext
es mucho más fácil: la publicación de la devolución de llamada utilizando Post
solo ocurrirá si el hilo aún existe, y las llamadas síncronas que usan Send
throws InvalidAsynchronousStateException
si el hilo ya no existe.
¿No es más fácil usar WindowsFormsSynchronizationContext
? ¿Me estoy perdiendo de algo? ¿Por qué debería usar el patrón InvokeRequirir-BeginInvoke si no es realmente seguro para subprocesos? ¿Qué piensas que es mejor?
¿Quién dijo que se prefiere InvokeRequired
/ Control.BeginInvoke
? Si me preguntas, en la mayoría de los casos es un patrón anti por las razones exactas que mencionaste. La pregunta a la que se vinculó tiene muchas respuestas, y algunas realmente sugieren utilizar el contexto de sincronización (incluido el mío ).
Si bien es cierto que cualquier control dado podría eliminarse antes de que intentes acceder al delegado publicado, eso se resuelve fácilmente usando Control.IsDisposed
(como tu delegado se está ejecutando en el hilo de UI para que nada pueda eliminar los controles mientras se ejecuta )
public partial class MyForm : Form
{
private readonly SynchronizationContext _context;
public MyForm()
{
_context = SynchronizationContext.Current
//...
}
private MethodOnOtherThread()
{
//...
_context.Post(status =>
{
// I think it''s enough to check the form''s IsDisposed
// But if you want to be extra paranoid you can test someLabel.IsDisposed
if (!IsDisposed) {someLabel.Text = newText;}
},null);
}
}