multithreading - Un problema de subprocesamiento donde mono se cuelga y MS.Net no
invoke begininvoke (3)
¿Sabes de dónde viene esta discrepancia? ¿Cómo lo resolverías?
No estoy seguro. No veo nada obvio en su código que pueda causar la diferencia entre Mono y .NET. Si tuviera que adivinar, diría que existe la posibilidad de que hayas tropezado con un error oscuro en Mono. Sin embargo, supongo que es posible que Mono utilice un mecanismo suficientemente diferente para manejar los mensajes WM_PAINT que hacen que el formulario se actualice. La pulsación constante del subproceso de interfaz de usuario de las llamadas repetidas a Invoke
puede interrumpir la capacidad de Mono de actualizar el formulario.
Y finalmente, ¿por qué BeginInvoke no funciona aquí?
Llamar a Invoke
en un círculo cerrado es suficientemente malo, pero BeginInvoke
será aún peor. El hilo de trabajo está inundando la bomba de mensaje de IU. BeginInvoke
no espera hasta que el subproceso de interfaz de usuario haya terminado de ejecutar el delegado. Simplemente publica las solicitudes y regresa rápidamente. Es por eso que parece colgarse. Los mensajes que BeginInvoke
está publicando en la cola de mensajes de la interfaz de usuario siguen desarrollándose, ya que es probable que el hilo de trabajo acelere la capacidad del subproceso de la interfaz de usuario para procesarlos.
Otros comentarios
También debo mencionar que el hilo de trabajo es casi inútil en el código. La razón es porque tiene una llamada a Invoke
en cada iteración. Invoke
bloques hasta que la UI haya terminado de ejecutar el delegado. Eso significa que su subproceso de trabajo y su subproceso de interfaz de usuario están esencialmente en sincronía entre ellos. En otras palabras, el trabajador pasa la mayor parte del tiempo esperando la interfaz de usuario y viceversa.
Solución
Una posible solución es ralentizar la velocidad a la que se llama Invoke
. En lugar de invocarlo en cada iteración de bucle intente hacerlo cada 1000 iteraciones o similar.
Cualquier enfoque aún mejor es no usar Invoke
o BeginInvoke
en absoluto. Personalmente, creo que estos mecanismos para actualizar la interfaz de usuario son demasiado utilizados. Casi siempre es mejor dejar que el subproceso de la interfaz de usuario acelere su propia tasa de actualización, especialmente cuando el subproceso de trabajo está en proceso continuo. Esto significa que deberá colocar un temporizador en el formulario y hacer que marque con la frecuencia de actualización deseada. Desde el evento Tick
probará una estructura de datos compartida que el hilo de trabajo está actualizando y usará esa información para actualizar los controles en el formulario. Esto tiene varias ventajas.
- Rompe el estrecho acoplamiento entre la interfaz de usuario y los hilos de trabajo que
Control.Invoke
impone. - Pone la responsabilidad de actualizar el hilo de la interfaz de usuario en el hilo de la interfaz de usuario donde debe pertenecer de todos modos.
- El subproceso de interfaz de usuario determina cuándo y con qué frecuencia debe llevarse a cabo la actualización.
- No hay riesgo de que la bomba de mensajes de IU se desborde, como sería el caso con las técnicas de clasificación iniciadas por el hilo de trabajo.
- El hilo de trabajo no tiene que esperar un reconocimiento de que se realizó la actualización antes de continuar con sus próximos pasos (es decir, obtiene más rendimiento tanto en la interfaz de usuario como en los hilos de trabajo).
Estoy probando mi aplicación con mono en previsión de un puerto Linux, y tengo un problema de enhebrado. Inicialmente consideré pegar 3000 líneas de código aquí, pero finalmente he ideado un pequeño ejemplo mínimo;)
Tienes un formulario con un botón (llamado poéticamente Button1
y una etiqueta (que lleva, sin sorpresa, el nombre Label1
)). Todo el lote está viviendo una vida feliz en un formulario llamado Form1
. Al hacer clic en Button1
inicia un ciclo infinito que incrementa un contador local y actualiza Label1
(usando Invoke
) para reflejar su valor.
Ahora en Mono, si cambia el tamaño del formulario, la etiqueta deja de actualizarse y nunca se reinicia. Esto no ocurre con la implementación de MS. BeginInvoke
no funciona mejor; peor, hace que la IU cuelgue en ambos casos.
¿Sabes de dónde viene esta discrepancia? ¿Cómo lo resolverías? Y finalmente, ¿por qué BeginInvoke no funciona aquí? Debo estar cometiendo un gran error ... ¿pero cuál?
EDITAR : Algunos progresos hasta el momento:- Llamar a BeginInvoke de hecho funciona; solo, la IU simplemente no se actualiza lo suficientemente rápido, por lo que parece detenerse.
- En mono, lo que sucede es que todo el hilo se cuelga cuando inserta un mensaje en la cola de la interfaz de usuario (por ejemplo, cambiando el tamaño del formulario). De hecho, la llamada
Invoke
síncrona nunca regresa. Estoy tratando de entender por qué. - De interés: incluso utilizando
BeginInvoke
, las llamadas asincrónicas no se ejecutan antes de que finalice la operación de cambio de tamaño. En MS.Net, siguen funcionando mientras cambian de tamaño.
El código se ve así (versión C # inferior):
Public Class Form1
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim T As New Threading.Thread(AddressOf Increment)
T.Start()
End Sub
Sub UpdateLabel(ByVal Text As String)
Label1.Text = Text
End Sub
Delegate Sub UpdateLabelHandler(ByVal Text As String)
Sub Increment()
Dim i As Long = 0
Dim UpdateLabelDelegate As New UpdateLabelHandler(AddressOf UpdateLabel)
Try
While True
i = (i + 1) Mod (Long.MaxValue - 1)
Me.Invoke(UpdateLabelDelegate, New Object() {i.ToString})
End While
Catch Ex As ObjectDisposedException
End Try
End Sub
End Class
O, en C #,
public class Form1
{
private void Button1_Click(System.Object sender, System.EventArgs e)
{
System.Threading.Thread T = new System.Threading.Thread(Increment);
T.Start();
}
public void UpdateLabel(string Text)
{
Label1.Text = Text;
}
public delegate void UpdateLabelHandler(string Text);
public void Increment()
{
long i = 0;
UpdateLabelHandler UpdateLabelDelegate = new UpdateLabelHandler(UpdateLabel);
try {
while (true) {
i = (i + 1) % (long.MaxValue - 1);
this.Invoke(UpdateLabelDelegate, new object[] { i.ToString() });
}
} catch (ObjectDisposedException Ex) {
}
}
}
Este es un error en el tiempo de ejecución mono, al menos creo que lo es. El código puede no ser una buena práctica (no soy un experto en subprocesos), pero lo que sugiere un error es el hecho de que el comportamiento difiere en Windows y Linux.
En Linux, mono tiene exactamente el mismo comportamiento que MS.Net tiene en Windows. Sin colgar, actualizaciones continuas incluso al cambiar el tamaño.
En Windows, mono muestra todos los problemas mencionados anteriormente. He publicado un informe de error en https://bugzilla.novell.com/show_bug.cgi?id=690400 .
Lo primero y más importante: hacer clic en Button1 ya es asincrónico, por lo que no es necesario crear otro thread para incrementar, simplemente llame al método de incremento. Lo siento, estaba leyendo su pregunta línea por línea y para cuando llegué al ciclo while Me olvidé del botón:
private void Button1_Click(System.Object sender, System.EventArgs e)
{
Thread t = new Thread(Increment);
t.IsBackground = true;
t.Start();
}
Segundo: si necesita usar un hilo, siempre debe establecer su hilo en segundo plano (es decir, el primer plano evita que su proceso finalice ), a menos que tenga una buena razón para usar un hilo de primer plano.
Tercero: si está realizando actualizaciones en la interfaz de usuario, entonces debe verificar la propiedad InvokeRequired
y llamar a BeginInvoke
:
public void UpdateLabel(string Text)
{
if (InvokeRequired)
{
BeginInvoke(new UpdateLabelDelegate(UpdateLabel), Text);
}
else
{
Label1.Text = Text;
}
}
public void Increment()
{
int i = 0;
while(true)
{
i++; // just incrementing i??
UpdateLabel(i.ToString());
Thread.Sleep(1000);// slow down a bit so you can see the updates
}
}
También puede "automatizar" el "patrón" Invocar requerido: Automatizar el patrón de código InvokeRequired
Y ahora mira si todavía tienes el mismo problema.
Lo probé en mi máquina y funciona como un encanto:
public partial class Form1 : Form
{
private delegate void UpdateLabelDelegate(string text);
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
Thread t = new Thread(Increment);
t.IsBackground = true;
t.Start();
}
private void UpdateLabel(string text)
{
if (label1.InvokeRequired)
{
BeginInvoke(new UpdateLabelDelegate(UpdateLabel), text);
}
else
{
label1.Text = text;
}
}
private void Increment()
{
int i = 0;
while (true)
{
i++;
UpdateLabel(i.ToString());
Thread.Sleep(1000);
}
}
}