¿Cómo puedo ejecutar código en un hilo en segundo plano y aún así acceder a la interfaz de usuario?

Resuelto orr burgel asked hace 7 años • 2 respuestas

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 form2llamada " ", 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.

orr burgel avatar Aug 08 '17 03:08 orr burgel
Aceptado

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_PAINTes 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 configure Enabled = 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 Threadclase 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 IsBackgroundpropiedad en Truesi 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.InvokeRequiredpropiedad , 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/Invokese 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 UpdateTextBoxDelegateque apunta a nuestro UpdateTextBoxmé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 InvokeRequiredverifique 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-Elsese 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!

Visual Vincent avatar Aug 08 '2017 14:08 Visual Vincent

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/

Sam Makin avatar Aug 07 '2017 20:08 Sam Makin