Compartir a través de


Cómo realizar llamadas seguras para subprocesos a controles

Multithreading puede mejorar el rendimiento de las aplicaciones de Windows Forms, pero el acceso a los controles de Windows Forms no es inherentemente seguro para subprocesos. Multithreading puede exponer el código a errores graves y complejos. Dos o más subprocesos que manipulan un control pueden forzar el mismo a un estado incoherente y provocar condiciones de carrera, interbloqueos y congelaciones. Si implementa multihilo en su aplicación, asegúrese de controlar acciones entre hilos de manera segura. Para obtener más información, consulte prácticas recomendadas de hilos administrados.

Hay dos maneras de llamar de forma segura a un control de Windows Forms desde un subproceso que no creó ese control. Use el System.Windows.Forms.Control.Invoke método para llamar a un delegado creado en el subproceso principal, que a su vez llama al control. O bien, implemente un System.ComponentModel.BackgroundWorker, que usa un modelo controlado por eventos para separar el trabajo realizado en el subproceso en segundo plano de informar sobre los resultados.

Llamadas no seguras entre subprocesos

No es seguro llamar a un control directamente desde un subproceso que no lo creó. En el fragmento de código siguiente se muestra una llamada no segura al System.Windows.Forms.TextBox control . El Button1_Click controlador de eventos crea un nuevo WriteTextUnsafe subproceso, que establece directamente la propiedad del TextBox.Text subproceso principal.

private void button1_Click(object sender, EventArgs e)
{
    var thread2 = new System.Threading.Thread(WriteTextUnsafe);
    thread2.Start();
}

private void WriteTextUnsafe() =>
    textBox1.Text = "This text was set unsafely.";
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim thread2 As New System.Threading.Thread(AddressOf WriteTextUnsafe)
    thread2.Start()
End Sub

Private Sub WriteTextUnsafe()
    TextBox1.Text = "This text was set unsafely."
End Sub

El depurador de Visual Studio detecta estas llamadas a subprocesos no seguras al generar un InvalidOperationException con el mensaje Operación entre subprocesos no válida. Control accesado desde un subproceso diferente al en el que fue creado. El InvalidOperationException siempre se produce para las llamadas entre subprocesos no seguras durante la depuración de Visual Studio y puede ocurrir también en el tiempo de ejecución de la aplicación. Debe corregir el problema, pero puede deshabilitar la excepción estableciendo la propiedad Control.CheckForIllegalCrossThreadCalls en false.

Llamadas seguras a través de hilos

En los ejemplos de código siguientes se muestran dos maneras de llamar de forma segura a un control de Windows Forms desde un subproceso que no lo creó:

  1. Método System.Windows.Forms.Control.Invoke, que llama a un delegado desde el hilo principal para invocar al control.
  2. Un System.ComponentModel.BackgroundWorker componente, que ofrece un modelo controlado por eventos.

En ambos ejemplos, el subproceso en segundo plano se suspende durante un segundo para simular el trabajo que se realiza en ese subproceso.

Ejemplo: Uso del método Invoke

En el ejemplo siguiente se muestra un patrón para garantizar llamadas seguras para subprocesos a un control de Windows Forms. Consulta la System.Windows.Forms.Control.InvokeRequired propiedad, que compara el identificador de subproceso de creación del control con el identificador de subproceso que hace la llamada. Si son diferentes, debería llamar al método Control.Invoke.

WriteTextSafe permite establecer un nuevo valor para la propiedad TextBox del control Text. El método consulta InvokeRequired. Si InvokeRequired devuelve true, WriteTextSafe se llama recursivamente a sí mismo, pasando el método como delegado al Invoke método . Si InvokeRequired devuelve false, WriteTextSafe establece directamente TextBox.Text . El Button1_Click controlador de eventos crea el nuevo subproceso y ejecuta el WriteTextSafe método .

private void button1_Click(object sender, EventArgs e)
{
    var threadParameters = new System.Threading.ThreadStart(delegate { WriteTextSafe("This text was set safely."); });
    var thread2 = new System.Threading.Thread(threadParameters);
    thread2.Start();
}

public void WriteTextSafe(string text)
{
    if (textBox1.InvokeRequired)
    {
        // Call this same method but append THREAD2 to the text
        Action safeWrite = delegate { WriteTextSafe($"{text} (THREAD2)"); };
        textBox1.Invoke(safeWrite);
    }
    else
        textBox1.Text = text;
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Dim threadParameters As New System.Threading.ThreadStart(Sub()
                                                                 WriteTextSafe("This text was set safely.")
                                                             End Sub)

    Dim thread2 As New System.Threading.Thread(threadParameters)
    thread2.Start()

End Sub

Private Sub WriteTextSafe(text As String)

    If (TextBox1.InvokeRequired) Then

        TextBox1.Invoke(Sub()
                            WriteTextSafe($"{text} (THREAD2)")
                        End Sub)

    Else
        TextBox1.Text = text
    End If

End Sub

Ejemplo: Usar un BackgroundWorker

Una manera fácil de implementar multithreading es con el System.ComponentModel.BackgroundWorker componente , que usa un modelo controlado por eventos. El subproceso en segundo plano genera el BackgroundWorker.DoWork evento , que no interactúa con el subproceso principal. El hilo principal ejecuta los controladores de eventos BackgroundWorker.ProgressChanged y BackgroundWorker.RunWorkerCompleted, los cuales pueden llamar a los controles del hilo principal.

Para realizar una llamada segura para subprocesos mediante BackgroundWorker, controle el evento DoWork. Hay dos eventos que utiliza el trabajador en segundo plano para notificar el estado: ProgressChanged y RunWorkerCompleted. El evento ProgressChanged se usa para comunicar las actualizaciones de estado al hilo principal, y el evento RunWorkerCompleted se usa para indicar que el trabajador en segundo plano ha terminado su trabajo. Para iniciar el subproceso en segundo plano, llame a BackgroundWorker.RunWorkerAsync.

El ejemplo cuenta de 0 a 10 durante el evento DoWork, pausando durante un segundo entre cuentas. Utiliza el ProgressChanged controlador de eventos para comunicar el número de regreso al subproceso principal y establecer la propiedad TextBox del control Text. Para que el evento ProgressChanged funcione, la propiedad BackgroundWorker.WorkerReportsProgress debe establecerse en true.

private void button1_Click(object sender, EventArgs e)
{
    if (!backgroundWorker1.IsBusy)
        backgroundWorker1.RunWorkerAsync();
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    int counter = 0;
    int max = 10;

    while (counter <= max)
    {
        backgroundWorker1.ReportProgress(0, counter.ToString());
        System.Threading.Thread.Sleep(1000);
        counter++;
    }
}

private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) =>
    textBox1.Text = (string)e.UserState;
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    If (Not BackgroundWorker1.IsBusy) Then
        BackgroundWorker1.RunWorkerAsync()
    End If

End Sub

Private Sub BackgroundWorker1_DoWork(sender As Object, e As ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork

    Dim counter = 0
    Dim max = 10

    While counter <= max

        BackgroundWorker1.ReportProgress(0, counter.ToString())
        System.Threading.Thread.Sleep(1000)

        counter += 1

    End While

End Sub

Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
    TextBox1.Text = e.UserState
End Sub