vb.net multithreading windows-7 crash focus

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



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 configure Enabled = 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!