¿Cómo puedo ejecutar código en un hilo en segundo plano y aún así acceder a la interfaz de usuario?
Hice un programa de búsqueda de archivos en Visual Studio en Windows 10 usando .net lang. Mi problema comienza desde form1 con una dim frm2 as form2 = new form2
llamada " ", después de que se muestra el nuevo formulario, inicio un bucle while en form1 que introduce datos en un cuadro de lista en el formulario 2. :
1) formulario1 llame al formulario2 y muéstrelo.
2) form1 inicia un ciclo while.
3) dentro del bucle while, los datos se envían a listbox1 en frm2
Ahora todo funciona en Windows 10 , el bucle while puede ejecutarse tanto como sea necesario sin ningún problema, la ventana puede perder el foco y recuperar el foco sin mostrar ningún problema."Not Responding.." msgs or white\black screens..
Pero, cuando llevo el software a la computadora de mi amigo que ejecuta Windows 7 , instalo todos los marcos necesarios y Visual Studio, lo ejecuto desde .sln en modo de depuración y hago la misma búsqueda en la misma carpeta, los resultados son:
1) el bucle while se ejecuta sin problemas siempre que el formulario 2 no pierda el foco (algo que no sucede en Windows 10)
2) cuando hago clic en cualquier parte de la pantalla, el software pierde el foco, lo que causa 1) que suceda (pantalla negra\pantalla blanca\no responde, etc.)
3) si espero el tiempo necesario para el bucle y no hago clic en ningún otro lugar, sigue funcionando sin problemas, actualizando una etiqueta como debería con la cantidad de archivos encontrados... e incluso termino el bucle con 100% de éxito (nuevamente a menos que haga clic en algún lugar). )
Ejemplo de código:
Sub ScanButtonInForm1()
Dim frm2 As Form2 = New Form2
frm2.Show()
Dim AlreadyScanned As HashSet(Of String) = New HashSet(Of String)
Dim stack As New Stack(Of String)
stack.Push("...Directoy To Start The Search From...")
Do While (stack.Count > 0)
frm2.Label4.Text = "-- Mapping Files... -- Folders Left:" + stack.Count.ToString + " -- Files Found:" + frm2.ListBox1.Items.Count.ToString + " --"
frm2.Label4.Refresh()
Dim ScanDir As String = stack.Pop
If AlreadyScanned.Add(ScanDir) Then
Try
Try
Try
Dim directoryName As String
For Each directoryName In System.IO.Directory.GetDirectories(ScanDir)
stack.Push(directoryName)
frm2.Label4.Text = "-- Mapping Files... -- Folders Left:" + stack.Count.ToString + " -- Files Found:" + frm2.ListBox1.Items.Count.ToString + " --"
frm2.Label4.Refresh()
Next
frm2.ListBox1.Items.AddRange(System.IO.Directory.GetFiles(ScanDir, "*.*", System.IO.SearchOption.AllDirectories))
Catch ex5 As UnauthorizedAccessException
End Try
Catch ex2 As System.IO.PathTooLongException
End Try
Catch ex4 As System.IO.DirectoryNotFoundException
End Try
End If
Loop
End Sub
¡Mis conclusiones fueron simples!
1) Windows 7 no admite la actualización de la interfaz de usuario (etiqueta) en vivo desde un bucle while llamado desde un botón...
2) Windows 7 posiblemente podría admitir un nuevo hilo que ejecute el mismo bucle
Creo que tal vez si ejecuto todo el código en un hilo, tal vez la interfaz de usuario seguirá respondiendo
( por cierto, la interfaz de usuario no responde en Windows 10 , pero todavía veo que la etiqueta se actualiza y nada falla cuando se pierde el foco ... )
Entonces sé cómo hacerlo, pero también sé que si lo hago, un hilo no podrá actualizar un cuadro de lista o una etiqueta en un formulario y actualizarlo.
entonces, el hilo necesitará actualizar un archivo externo con los datos y el formulario 2 necesitará leer esos datos en vivo desde el archivo, pero ¿provocará los mismos problemas? No tengo idea de qué hacer. Me vendría bien un poco de ayuda y consejos. ¡GRACIAS!
Debo mencionar el hecho de que el bucle funciona en Windows 10 sin una interfaz de usuario receptiva, lo que significa que no puedo hacer clic en ningún botón pero aún puedo ver la etiqueta actualizar PERO en Windows 7 todo funciona igual A MENOS que haga clic en algún lugar, sin importar dónde haga clic. en Windows el bucle falla
Estoy usando el desarrollador Framework 4.6.2.
Si bien me alegra que hayas encontrado una solución, te desaconsejo usarla Application.DoEvents()
porque es una mala práctica .
Consulte esta publicación de blog: Cómo mantener la capacidad de respuesta de su interfaz de usuario y los peligros de Application.DoEvents .
En pocas palabras, Application.DoEvents()
es una solución sucia que hace que su interfaz de usuario parezca receptiva porque obliga al hilo de la interfaz de usuario a manejar todos los mensajes de ventana disponibles actualmente. WM_PAINT
es uno de esos mensajes por lo que su ventana se vuelve a dibujar.
Sin embargo, esto tiene algunos inconvenientes... Por ejemplo:
Si cerrara el formulario durante este proceso "en segundo plano", lo más probable es que arrojara un error.
Otro inconveniente es que si el
ScanButtonInForm1()
método se llama haciendo clic en un botón, podrá volver a hacer clic en ese botón (a menos que configureEnabled = False
) y comenzar el proceso una vez más, lo que nos lleva a otro inconveniente:Cuantos más
Application.DoEvents()
bucles inicie, más ocupará el subproceso de la interfaz de usuario, lo que hará que el uso de la CPU aumente con bastante rapidez. Dado que cada bucle se ejecuta en el mismo subproceso, su procesador no puede programar el trabajo en diferentes núcleos ni subprocesos, por lo que su código siempre se ejecutará en un núcleo, consumiendo la mayor cantidad de CPU posible.
El reemplazo es, por supuesto, un subproceso múltiple adecuado (o la Biblioteca paralela de tareas , lo que prefiera). En realidad, el multiproceso regular no es tan difícil de implementar.
Los basicos
Para crear un nuevo hilo solo necesitas declarar una instancia de la Thread
clase y pasar un delegado al método que deseas que ejecute el hilo:
Dim myThread As New Thread(AddressOf <your method here>)
... entonces debes establecer su IsBackground
propiedad en True
si quieres que se cierre automáticamente cuando se cierre el programa (de lo contrario, mantendrá el programa abierto hasta que finalice el hilo).
¡ Luego simplemente llamas Start()
y tendrás un hilo en segundo plano en ejecución!
Dim myThread As New Thread(AddressOf myThreadMethod)
myThread.IsBackground = True
myThread.Start()
Accediendo al hilo de la interfaz de usuario
La parte complicada del subproceso múltiple es reunir las llamadas al subproceso de la interfaz de usuario. Un subproceso en segundo plano generalmente no puede acceder a elementos (controles) en el subproceso de la interfaz de usuario porque eso podría causar problemas de concurrencia (dos subprocesos acceden al mismo control al mismo tiempo). Por lo tanto, debe ordenar sus llamadas a la UI programándolas para su ejecución en el propio subproceso de la UI . De esa manera, ya no correrá el riesgo de concurrencia porque todo el código relacionado con la interfaz de usuario se ejecuta en el subproceso de la interfaz de usuario.
Para clasificar las llamadas al subproceso de la interfaz de usuario, utilice cualquiera de los métodos Control.Invoke()
o Control.BeginInvoke()
. BeginInvoke()
es la versión asincrónica , lo que significa que no espera a que se complete la llamada de la interfaz de usuario antes de permitir que el subproceso en segundo plano continúe con su trabajo.
También debe asegurarse de verificar la Control.InvokeRequired
propiedad , que le indica si ya está en el hilo de la interfaz de usuario (en cuyo caso invocarlo es extremadamente innecesario) o no.
El patrón básico InvokeRequired/Invoke
se ve así (principalmente como referencia, sigue leyendo a continuación para conocer formas más breves):
'This delegate will be used to tell Control.Invoke() which method we want to invoke on the UI thread.
Private Delegate Sub UpdateTextBoxDelegate(ByVal TargetTextBox As TextBox, ByVal Text As String)
Private Sub myThreadMethod() 'The method that our thread runs.
'Do some background stuff...
If Me.InvokeRequired = True Then '"Me" being the current form.
Me.Invoke(New UpdateTextBoxDelegate(AddressOf UpdateTextBox), TextBox1, "Status update!") 'We are in a background thread, therefore we must invoke.
Else
UpdateTextBox(TextBox1, "Status update!") 'We are on the UI thread, no invoking required.
End If
'Do some more background stuff...
End Sub
'This is the method that Control.Invoke() will execute.
Private Sub UpdateTextBox(ByVal TargetTextBox As TextBox, ByVal Text As String)
TargetTextBox.Text = Text
End Sub
New UpdateTextBoxDelegate(AddressOf UpdateTextBox)
crea una nueva instancia de UpdateTextBoxDelegate
que apunta a nuestro UpdateTextBox
método (el método a invocar en la interfaz de usuario).
Sin embargo, a partir de Visual Basic 2010 (10.0) y superiores , puede usar expresiones Lambda , lo que facilita mucho la invocación :
Private Sub myThreadMethod()
'Do some background stuff...
If Me.InvokeRequired = True Then '"Me" being the current form.
Me.Invoke(Sub() TextBox1.Text = "Status update!") 'We are in a background thread, therefore we must invoke.
Else
TextBox1.Text = "Status update!" 'We are on the UI thread, no invoking required.
End If
'Do some more background stuff...
End Sub
Ahora todo lo que tienes que hacer es escribir Sub()
y luego continuar escribiendo código como si estuvieras en un método normal:
If Me.InvokeRequired = True Then
Me.Invoke(Sub()
TextBox1.Text = "Status update!"
Me.Text = "Hello world!"
Label1.Location = New Point(128, 32)
ProgressBar1.Value += 1
End Sub)
Else
TextBox1.Text = "Status update!"
Me.Text = "Hello world!"
Label1.Location = New Point(128, 32)
ProgressBar1.Value += 1
End If
¡Y así es como diriges las llamadas al hilo de la interfaz de usuario!
Haciéndolo más simple
Para que sea aún más sencillo invocar a la interfaz de usuario, puede crear un método de extensión que invoque y InvokeRequired
verifique por usted.
Coloque esto en un archivo de código separado:
Imports System.Runtime.CompilerServices
Public Module Extensions
''' <summary>
''' Invokes the specified method on the calling control's thread (if necessary, otherwise on the current thread).
''' </summary>
''' <param name="Control">The control which's thread to invoke the method at.</param>
''' <param name="Method">The method to invoke.</param>
''' <param name="Parameters">The parameters to pass to the method (optional).</param>
''' <remarks></remarks>
<Extension()> _
Public Function InvokeIfRequired(ByVal Control As Control, ByVal Method As [Delegate], ByVal ParamArray Parameters As Object()) As Object
If Parameters IsNot Nothing AndAlso _
Parameters.Length = 0 Then Parameters = Nothing
If Control.InvokeRequired = True Then
Return Control.Invoke(Method, Parameters)
Else
Return Method.DynamicInvoke(Parameters)
End If
End Function
End Module
Ahora solo necesita llamar a este método único cuando desee acceder a la interfaz de usuario, no If-Then-Else
se requiere nada adicional:
Private Sub myThreadMethod()
'Do some background stuff...
Me.InvokeIfRequired(Sub()
TextBox1.Text = "Status update!"
Me.Text = "Hello world!"
Label1.Location = New Point(128, 32)
End Sub)
'Do some more background stuff...
End Sub
Devolver objetos/datos de la interfaz de usuario conInvokeIfRequired()
Con mi InvokeIfRequired()
método de extensión también puedes devolver objetos o datos del hilo de la interfaz de usuario de una manera sencilla. Por ejemplo, si quieres el ancho de una etiqueta:
Dim LabelWidth As Integer = Me.InvokeIfRequired(Function() Label1.Width)
Ejemplo
El siguiente código incrementará un contador que le indicará cuánto tiempo se ha ejecutado el hilo:
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
Dim CounterThread As New Thread(AddressOf CounterThreadMethod)
CounterThread.IsBackground = True
CounterThread.Start()
Button1.Enabled = False 'Make the button unclickable (so that we cannot start yet another thread).
End Sub
Private Sub CounterThreadMethod()
Dim Time As Integer = 0
While True
Thread.Sleep(1000) 'Wait for approximately 1000 ms (1 second).
Time += 1
Me.InvokeIfRequired(Sub() Label1.Text = "Thread has been running for: " & Time & " seconds.")
End While
End Sub
¡Espero que esto ayude!
La razón por la que su aplicación se congela es que usted está haciendo todo el trabajo en el subproceso de la interfaz de usuario. Consulte Async y Await. Utiliza subprocesos en segundo plano pero lo hace mucho más fácil de administrar. Un ejemplo aquí:
https://stephenhaunts.com/2014/10/14/using-async-and-await-to-update-the-ui-thread/