vb.net - ¿Cómo puedo ejecutar código en un subproceso en segundo plano y aún acceder a la interfaz de usuario?
multithreading windows-7 (2)
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
"
dim frm2 as form2 = new form2
", después de que se muestra el nuevo formulario, comienzo un ciclo while en form1 que se alimenta datos en un cuadro de lista en el formulario 2:
1) form1 llama a form2 y muéstralo.
2) form1 comienza un ciclo while.
3) dentro de los datos del bucle while que se envían a listbox1 en frm2
Ahora todo funciona en
Windows 10
, el ciclo while puede ejecutarse todo lo que necesite sin ningún problema, la ventana puede perder el foco y recuperar el foco
sin
mostrar ningún
"Not Responding.." msgs or white/black screens..
Pero, cuando llevo el software a mi computadora amiga que ejecuta Windows 7 , instalo todos los frameworks necesarios y Visual Studio, lo ejecuto desde el .sln en modo de depuración y hago la misma búsqueda en la misma carpeta, los resultados son:
1) el ciclo while se ejecuta sin problemas siempre que la forma 2 no pierda el foco (algo que no sucede en Windows 10)
2) cuando hago clic en cualquier lugar de la pantalla, el software pierde el foco lo que hace que 1) 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 finalizo el bucle con un 100% de éxito (de nuevo 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 en vivo de la interfaz de usuario (etiqueta) desde un ciclo while llamado desde un botón ...
2) Windows 7 podría admitir un nuevo hilo que ejecute el mismo bucle
Creo que mabye si ejecuto todo el código en un hilo mabye, 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 se bloquea cuando se suelta el foco ... )
así que 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 formulario2 necesitará leer esos datos en vivo desde el archivo, pero ¿tendrá los mismos problemas? No tengo idea de qué hacer ... puedo usar alguna ayuda y consejos. ¡GRACIAS!
Debo mencionar el hecho de que el bucle funciona en Windows 10 sin una interfaz de usuario sensible, 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 de que haga clic en algún lugar, sin importar dónde haga clic en windows el bucle se bloquea
estoy usando el desarrollador de framework 4.6.2
La razón por la que su aplicación se está congelando es porque está haciendo todo el trabajo en el hilo de la interfaz de usuario. Echa un vistazo a Async y Await. Utiliza subprocesos en segundo plano pero hace que sea 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/
Si bien me alegro de que hayas encontrado una solución, te aconsejo que no uses
Application.DoEvents()
porque
es una mala práctica
.
Consulte esta publicación de blog: Mantener su interfaz de usuario receptiva 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, razón por la cual su ventana se vuelve a dibujar.
Sin embargo, esto tiene algunas desventajas ... Por ejemplo:
-
Si cerrara el formulario durante este proceso de "fondo", lo más probable es que arroje un error.
-
Otra parte trasera es que si se llama al método
ScanButtonInForm1()
al hacer clic en un botón, podrá volver a hacer clic en ese botón (a menos que configureEnabled = False
) e iniciar el proceso una vez más, lo que nos lleva a otra parte trasera. : -
Cuantos más bucles
Application.DoEvents()
inicies, más ocuparás el hilo de la interfaz de usuario, lo que hará que el uso de tu CPU aumente bastante rápido. Dado que cada bucle se ejecuta en el mismo subproceso, su procesador no puede programar el trabajo en diferentes núcleos o 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, el subprocesamiento múltiple adecuado (o la Biblioteca de tareas paralelas , lo que prefiera). El multihilo regular en realidad no es tan difícil de implementar.
Los basicos
Para crear un nuevo hilo, solo necesita declarar una instancia de la
clase
Thread
y pasar un delegado al método que desea que se ejecute:
Dim myThread As New Thread(AddressOf <your method here>)
... entonces debe establecer su
propiedad
IsBackground
en
True
si desea que se cierre automáticamente cuando el programa se cierre (de lo contrario, mantiene el programa abierto hasta que finalice el subproceso).
¡Entonces solo llama a
Start()
y tiene un hilo de fondo en ejecución!
Dim myThread As New Thread(AddressOf myThreadMethod)
myThread.IsBackground = True
myThread.Start()
Accediendo al hilo de la IU
La parte complicada de los subprocesos múltiples es ordenar las llamadas al hilo 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 puede causar problemas de concurrencia (dos subprocesos que acceden al mismo control al mismo tiempo). Por lo tanto, debe ordenar sus llamadas a la interfaz de usuario al programarlas para su ejecución en el hilo de la interfaz de usuario . De esta forma, ya no tendrá el riesgo de concurrencia porque todo el código relacionado con la interfaz de usuario se ejecuta en el hilo de la interfaz de usuario.
Para llamar a Marhsal al hilo de la interfaz de usuario, use 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
propiedad
Control.InvokeRequired
, que le indica si ya está en el hilo de la interfaz de usuario (en cuyo caso la invocación es
extremadamente
innecesaria) o no.
El
InvokeRequired/Invoke
básico
InvokeRequired/Invoke
tiene este aspecto (principalmente como referencia, sigue leyendo a continuación para ver formas más cortas):
''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 método
UpdateTextBox
(el método para invocar en la interfaz de usuario).
Sin embargo, a partir de Visual Basic 2010 (10.0) y superior , puede usar expresiones Lambda que hacen que la invocación sea mucho más fácil:
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 tiene que hacer es escribir
Sub()
y luego continuar escribiendo código como si estuviera en un método regular:
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 usted ordena 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
InvokeRequired
la invocación y la comprobación de invocación
InvokeRequired
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 se requiere
If-Then-Else
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 desde la interfaz de usuario con
InvokeIfRequired()
Con mi método de extensión
InvokeIfRequired()
también puede devolver objetos o datos del hilo de la interfaz de usuario de una manera simple.
Por ejemplo, si desea 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á durante cuánto tiempo se ha ejecutado el subproceso:
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!