A continuación encontrarás traducido al castellano el artículo escrito por Gabriel Ludosanu y publicado originalmente en el Blog oficial de Xojo.
En la primera parte aprendimos que los bucles con DoEvents “congelan” tu interfaz de usuario. En la segunda parte vimos el funcionamiento del control Timer. En la tercera parte movimos los controles Timer para que funcionasen desde código; mientras que en la cuarta parte vimos un funcionamiento simplificado mediante CallLater.
Pero los Timer tienen un límite: funcionan en el hilo principal de la app o, lo que es lo mismo, el mismo hilo sobre el que también se ejecuta la interfaz de usuario. Si tus necesidades de funcionamiento en segundo plano son realmente tareas pesadas, entonces un Timer no representará la mejor opción. En realidad necesitarás un verdadero procesamiento en segundo plano, y ahí es donde entran en juego los hilos.
La idea fundamental: Threads
Un hilo consiste en una ruta de ejecución independiente que no colisiona con el hilo utilizado por la interfaz de usuario. Mientras que tu hilo se encargará del trabajo duro, la interfaz de usuario se mantendrá activa ante las acciones del usuario. Una vez que la ejecución del hilo finalice, notificará al hilo de la interfaz de usuario mediante el método UserInterfaceUpdate, y sólo dicho callback se ejecutará sobre el hilo principal.
La regla es bastante simple: los hilos no pueden interactuar directamente con los controles de la interfaz de usuario. Si lo intentas obtendrás un cuelgue de la aplicación. El método UserInterfaceUpdate es el puente perfecto.
Para una mejor comprensión sobre el modo en el que funciona el modelo de hilos en Xojo, incluyendo las diferencias entre los hilos cooperativos y los preemptivos, consulta el artículo Cooperative to Preemptive: Weaving New Threads Into Your Apps.
¡Vamos con el código!
Pre-requisitos: Añade un StatusLabel (DesktopLabel), un DesktopButton y un control Thread. Define igualmente:
kCountTo As Integer = 100
El control Thread
Arrastra un Thread desde la Librería a la ventana del proyecto y cambia su nombre a backgroundWorker.
El Botón (iniciar el hilo)
Introduce el siguiente código en el evento Pressed del botón:
Sub Pressed()
If backgroundWorker.ThreadState = Thread.ThreadStates.NotRunning Then
backgroundWorker.Start
End If
End Sub
El anterior fragmento de código comprueba si el hilo ya está en funcionamiento. Si no es así, lo inicia. Evita que se apilen o acumulen múltiples hilos en el caso de que el usuario realice varios clic sobre el botón de forma rápida.
El método Thread.Run (tarea en segundo plano)
Implementa el método Run en la instancia de Thread añadida, incluyendo sobre dicho método el siguiente código:
Sub Run()
For i As Integer = 0 To kCountTo
' Do background work here. No UI access allowed.
// It can be: database query, file read, API call, pretty much anything that might block "freeze" the UI of the app
' Prepare a Dictionary payload for the UI
Var info As New Dictionary
info.Value("progress") = i
info.Value("message") = "working"
Me.AddUserInterfaceUpdate(info)
' Sleep (optional)
Thread.SleepCurrent(10) // Sleep 10 ms
Next
End Sub
La línea clave es Me.AddUserInterfaceUpdate(info). Este se encarga de encolar datos en el hilo de la interfaz de usuario de modo que los pueda procesar. Cada llamada añade un item a la cola, y el hilo de la interfaz de usuario se encargará de extraerlos y procesarlos en UserInterfaceUpdate.
El método Thread.UserInterfaceUpdate (sincronización con la IU)
Implementa este evento para gestionar las actualizaciones desde el hilo:
Sub UserInterfaceUpdate(data() As Dictionary)
// Runs on the main UI thread. Safe to update controls here.
' Guard against empty or malformed data
If data = Nil Or data.Count < 0 Then Return
Var d As Dictionary = data(0)
' Extract the progress value safely
Var progress As Integer = 0
If d.HasKey("progress") Then
progress = d.Value("progress").IntegerValue
End If
' Update the UI
StatusLabel.Text = progress.ToString
' Mark completion when we hit the target
If progress >= kCountTo Then
StatusLabel.Text = "done"
End If
End Sub
Anotaciones importantes:
- data() es un array de Diccionarios.
- Utiliza siempre HasKey si uno de los diccionarios contiene la clave esperada antes de recuperar el valor asociado.
- Utiliza .IntegerValue (o .StringValue, etc.) para extraer los valores con el tipo de datos esperado.
- Este se ejecuta en el hilo principal de la interfaz de usuario, de modo que resulta conveniente actualizar la etiqueta StatusLabel y cualquier otro control visual.
Como funciona
- Pulsación en el botón: haz clic en el botón para iniciar la ejecución del hilo.
- Bucle en segundo plano: el hilo itera desde 0 a el máximo definido en kCountTo, “durmiendo” 10 ms en cada paso. Tras cada uno de los pasos, este encola una actualización.
- Respuesta de la IU: durante la ejecución del hilo también podrás interaccionar con el resto de tu aplicación, sin que ls interfaz de usuario quede bloqueada.
Observación sobre la seguridad con hilos
Los hilos son potentes, pero también pueden resultar peligrosos si no se utilizan con cuidado. Estas son algunos límites:
- Ejecución segura en el método Run(): Cualquier código que no actúe sobre controles de interfaz de usuario, operaciones de E/S, consultas a bases de datos, cálculos, transferencias de red.
- Ejecución no segura en Run(): Leer o escribir directamente sobre las propiedades de controles de interfaz de usuario.
- Seguro en UserInterfaceUpdate(): Cualquier tipo de acceso a controles de interfaz de usuario. Esto se ejecuta en el hilo principal de la aplicación. Si no observas esta regla e intentas actualizar una etiqueta desde Run() tu aplicación se colgará. Pasa los datos mediante AddUserInterfaceUpdate.
Cuándo utilizar Hilos en vez de Timers
Usa Timers para:
- Tareas frecuentes y breves (10-50 ms).
- Actualizaciones de progreso sencillas.
Usa los hilos para:
- Operaciones de larga duración (segundos, minutos, horas).
- Tareas de segundo plano múltiples e independientes entre sí.
En esta serie de artículos hemos recorrido un largo camino, desde bloquear la interfaz de usuario al uso de temporizadores en el hilo de la interfaz de usuario, y finalmente el uso de hilos para ejecutar tareas en segundo plano. Cada una de las soluciones propuestas soluciona un tipo de problema diferente.
En resumen
Los hilos suponen la artillería pesada para lograr diseños ágiles sin bloques de la IU. Funcionan de forma independiente y se comunican de forma segura con el hilo principal de la app mediante UserInterfaceUpdate. Una vez que los domines podrás crear aplicaciones que no se queden “congeladas” y saquen el máximo partido de todos los núcleos disponibles en el procesador de tu equipo.
Consulta la documentación sobre Threads para obtener información adicional sobre la prioridad, tamaño de pila y patrones de diseño avanzados.