Cómo invocar un método de IU desde otro hilo

Jugando con temporizadores. Contexto: a winforms con dos tags.

Me gustaría ver cómo funciona System.Timers.Timer así que no he usado el temporizador Forms. Entiendo que el formulario y myTimer ahora se ejecutarán en diferentes hilos. ¿Hay una manera fácil de representar el tiempo transcurrido en lblValue en la siguiente forma?

He buscado aquí en MSDN, pero ¿hay una manera más fácil?

Aquí está el código de winforms:

 using System.Timers; namespace Ariport_Parking { public partial class AirportParking : Form { //instance variables of the form System.Timers.Timer myTimer; int ElapsedCounter = 0; int MaxTime = 5000; int elapsedTime = 0; static int tickLength = 100; public AirportParking() { InitializeComponent(); keepingTime(); lblValue.Text = "hello"; } //method for keeping time public void keepingTime() { myTimer = new System.Timers.Timer(tickLength); myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed); myTimer.AutoReset = true; myTimer.Enabled = true; myTimer.Start(); } void myTimer_Elapsed(Object myObject,EventArgs myEventArgs){ myTimer.Stop(); ElapsedCounter += 1; elapsedTime += tickLength; if (elapsedTime < MaxTime) { this.lblElapsedTime.Text = elapsedTime.ToString(); if (ElapsedCounter % 2 == 0) this.lblValue.Text = "hello world"; else this.lblValue.Text = "hello"; myTimer.Start(); } else { myTimer.Start(); } } } } 

Supongo que tu código es solo una prueba, así que no discutiré sobre lo que haces con tu temporizador. El problema aquí es cómo hacer algo con un control de interfaz de usuario dentro de la callback del temporizador.

A la mayoría de los métodos y propiedades de Control solo se puede acceder desde el hilo de la interfaz de usuario (en realidad, solo se puede acceder a ellos desde el hilo donde los creó, pero esta es otra historia). Esto se debe a que cada hilo debe tener su propio bucle de mensaje ( GetMessage() filtra los mensajes por hilo) y luego para hacer algo con un Control debe enviar un mensaje de su hilo al hilo principal . En .NET es fácil porque cada Control hereda un par de métodos para este fin: Invoke/BeginInvoke/EndInvoke . Para saber si la ejecución del hilo debe llamar a esos métodos, tiene la propiedad InvokeRequired . Simplemente cambie su código con esto para que funcione:

 if (elapsedTime < MaxTime) { this.BeginInvoke(new MethodInvoker(delegate { this.lblElapsedTime.Text = elapsedTime.ToString(); if (ElapsedCounter % 2 == 0) this.lblValue.Text = "hello world"; else this.lblValue.Text = "hello"; })); } 

Consulte MSDN para obtener la lista de métodos a los que puede llamar desde cualquier hilo, solo como referencia siempre puede llamar a Invalidate , BeginInvoke , EndInvoke , Invoke métodos y leer la propiedad InvokeRequired . En general, este es un patrón de uso común (suponiendo que this es un objeto derivado de Control ):

 void DoStuff() { // Has been called from a "wrong" thread? if (InvokeRequired) { // Dispatch to correct thread, use BeginInvoke if you don't need // caller thread until operation completes Invoke(new MethodInvoker(DoStuff)); } else { // Do things } } 

Tenga en cuenta que el hilo actual se bloqueará hasta que la UI enhebre la ejecución completa del método. Esto puede ser un problema si el tiempo del hilo es importante (no olvide que el hilo de la interfaz de usuario puede estar ocupado o colgado un poco). Si no necesita el valor de retorno del método, puede simplemente reemplazar Invoke con BeginInvoke , para WinForms ni siquiera necesita una llamada posterior a EndInvoke :

 void DoStuff() { if (InvokeRequired) { BeginInvoke(new MethodInvoker(DoStuff)); } else { // Do things } } 

Si necesita valor devuelto, entonces tiene que tratar con la interfaz IAsyncResult habitual.

¿Cómo funciona?

Una aplicación de Windows GUI se basa en el procedimiento de ventana con sus bucles de mensajes. Si escribe una aplicación en C simple tiene algo como esto:

 MSG message; while (GetMessage(&message, NULL, 0, 0)) { TranslateMessage(&message); DispatchMessage(&message); } 

Con estas pocas líneas de código, su aplicación espera un mensaje y luego entrega el mensaje al procedimiento de ventana. El procedimiento de ventana es una gran statement de cambio / caso donde usted verifica los mensajes ( WM_ ) que conoce y los procesa de alguna manera (usted pinta la ventana para WM_PAINT , abandona su aplicación para WM_QUIT etc.).

Ahora imagine que tiene un hilo funcional, ¿cómo puede llamar a su hilo principal? La forma más simple es usar esta estructura subyacente para hacer el truco. Simplifico demasiado la tarea, pero estos son los pasos:

  • Cree una cola de funciones (insegura de hilos) para invocar (algunos ejemplos aquí en SO ).
  • Publique un mensaje personalizado en el procedimiento de ventana. Si convierte esta cola en una cola de prioridad, puede incluso decidir la prioridad para estas llamadas (por ejemplo, una notificación de progreso de un hilo de trabajo puede tener una prioridad menor que una notificación de alarma).
  • En el procedimiento de ventana (dentro de la statement de cambio / caso) usted comprende ese mensaje y luego puede ver la función para llamar desde la cola e invocarla.

WPF y WinForms usan este método para entregar (despachar) un mensaje de un hilo al hilo de la interfaz de usuario. Eche un vistazo a este artículo en MSDN para obtener más detalles sobre múltiples hilos e interfaz de usuario, WinForms esconde muchos de estos detalles y no tiene que encargarse de ellos, pero puede ver cómo funciona bajo el capó.

Personalmente cuando trabajo en una aplicación que funciona con hilos fuera de la interfaz de usuario, generalmente escribo este pequeño fragmento.

 private void InvokeUI(Action a) { this.BeginInvoke(new MethodInvoker(a)); } 

Cuando hago una llamada asincrónica en un hilo diferente siempre puedo volver a llamar utilizando.

 InvokeUI(() => { Label1.Text = "Super Cool"; }); 

Simple y limpio.

Tal como lo solicité, esta es mi respuesta que busca las llamadas entre subprocesos, sincroniza las actualizaciones de las variables, no se detiene e inicia el temporizador y no utiliza el temporizador para contar el tiempo transcurrido.

EDIT solucionó la llamada a BeginInvoke . He hecho la invocación del hilo cruzado usando una Action genérica. Esto permite que se pasen el remitente y el eventargs. Si no se utilizan (como están aquí), es más eficiente usar MethodInvoker pero sospecho que el manejo debería moverse a un método sin parámetros.

 public partial class AirportParking : Form { private Timer myTimer = new Timer(100); private int elapsedCounter = 0; private readonly DateTime startTime = DateTime.Now; private const string EvenText = "hello"; private const string OddText = "hello world"; public AirportParking() { lblValue.Text = EvenText; myTimer.Elapsed += MyTimerElapsed; myTimer.AutoReset = true; myTimer.Enabled = true; myTimer.Start(); } private void MyTimerElapsed(object sender,EventArgs myEventArgs) { If (lblValue.InvokeRequired) { var self = new Action(MyTimerElapsed); this.BeginInvoke(self, new [] {sender, myEventArgs}); return; } lock (this) { lblElapsedTime.Text = DateTime.Now.SubTract(startTime).ToString(); elapesedCounter++; if(elapsedCounter % 2 == 0) { lblValue.Text = EvenText; } else { lblValue.Text = OddText; } } } } 

En primer lugar, en Windows Forms (y en la mayoría de los frameworks), solo se puede acceder a un control (a menos que esté documentado como “thread safe”) por el subproceso UI.

Entonces this.lblElapsedTime.Text = ... en su callback es completamente erróneo. Eche un vistazo a Control.BeginInvoke .

En segundo lugar, debe usar System.DateTime y System.TimeSpan para sus cálculos de tiempo.

No probado:

 DateTime startTime = DateTime.Now; void myTimer_Elapsed(...) { TimeSpan elapsed = DateTime.Now - startTime; this.lblElapsedTime.BeginInvoke(delegate() { this.lblElapsedTime.Text = elapsed.ToString(); }); } 

Terminó usando lo siguiente. Es una combinación de las sugerencias dadas:

 using System.Timers; namespace Ariport_Parking { public partial class AirportParking : Form { //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> //instance variables of the form System.Timers.Timer myTimer; private const string EvenText = "hello"; private const string OddText = "hello world"; static int tickLength = 100; static int elapsedCounter; private int MaxTime = 5000; private TimeSpan elapsedTime; private readonly DateTime startTime = DateTime.Now; //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< public AirportParking() { InitializeComponent(); lblValue.Text = EvenText; keepingTime(); } //method for keeping time public void keepingTime() { using (System.Timers.Timer myTimer = new System.Timers.Timer(tickLength)) { myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed); myTimer.AutoReset = true; myTimer.Enabled = true; myTimer.Start(); } } private void myTimer_Elapsed(Object myObject,EventArgs myEventArgs){ elapsedCounter++; elapsedTime = DateTime.Now.Subtract(startTime); if (elapsedTime.TotalMilliseconds < MaxTime) { this.BeginInvoke(new MethodInvoker(delegate { this.lblElapsedTime.Text = elapsedTime.ToString(); if (elapsedCounter % 2 == 0) this.lblValue.Text = EvenText; else this.lblValue.Text = OddText; })); } else {myTimer.Stop();} } } }