Procedimiento para realizar llamadas seguras para subprocesos en controles de Windows Forms

El multithreading puede mejorar el rendimiento de las aplicaciones de Windows Forms, pero el acceso a los controles de Windows Forms no es intrínsecamente seguro para los subprocesos. El multithreading puede exponer el código a errores muy graves y complejos. Dos o más subprocesos que manipulan un control pueden llevar al control a un estado incoherente y provocar condiciones de carrera, interbloqueos y bloqueos. Si implementa multithreading en la aplicación, asegúrese de llamar a controles entre subprocesos de una manera segura para los subprocesos. Para obtener más información, vea Procedimientos recomendados de subprocesos administrados.

Hay dos maneras de llamar de forma segura a un control de Windows Forms desde un subproceso que no ha creado ese control. Se puede usar el método System.Windows.Forms.Control.Invoke para llamar a un delegado creado en el subproceso principal, que a su vez llama al control. También se puede implementar un objeto System.ComponentModel.BackgroundWorker, que usa un modelo controlado por eventos para separar el trabajo realizado en el subproceso en segundo plano de la generación de informes sobre los resultados.

Llamadas no seguras entre subprocesos

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

private void Button1_Click(object sender, EventArgs e)
{
    thread2 = new Thread(new ThreadStart(WriteTextUnsafe));
    thread2.Start();
}
private void WriteTextUnsafe()
{
    textBox1.Text = "This text was set unsafely.";
}
Private Sub Button1_Click(ByVal sender As Object, e As EventArgs) Handles Button1.Click
    Thread2 = New Thread(New ThreadStart(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 no seguras a subprocesos mediante la generación de una excepción InvalidOperationException con el mensaje Operación no válida a través de subprocesos: Se tuvo acceso al control "" desde un subproceso distinto a aquel en que lo creó. La excepción InvalidOperationException se produce siempre para las llamadas entre subprocesos no seguras durante la depuración de Visual Studio y pueden producirse 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 entre subprocesos

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 ha creado:

  1. El método System.Windows.Forms.Control.Invoke, que llama a un delegado del subproceso principal para llamar al control.
  2. Un componente System.ComponentModel.BackgroundWorker, 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.

Estos ejemplos se pueden compilar y ejecutar como aplicaciones .NET Framework desde la línea de comandos de C# o Visual Basic. Para obtener más información, vea Compilar la línea de comandos con csc.exe o Compilar desde la línea de comandos (Visual Basic).

A partir de .NET Core 3.0, los ejemplos también se pueden compilar y ejecutar como aplicaciones .NET Core de Windows desde una carpeta que tenga un archivo de proyecto de Windows Forms para .NET Core denominado <nombre de carpeta>.csproj.

Ejemplo: usar el método Invoke con un delegado

En el ejemplo siguiente se muestra un patrón para garantizar llamadas seguras para subprocesos a un control de Windows Forms. En él, se consulta la propiedad System.Windows.Forms.Control.InvokeRequired, que compara el identificador de creación del subproceso del control con el identificador del subproceso que realiza la llamada. Si los identificadores de subproceso son los mismos, se llama al control directamente. Si los identificadores de subproceso son diferentes, se llama al método Control.Invoke con un delegado del subproceso principal, que realiza la llamada real al control.

El delegado SafeCallDelegate permite establecer la propiedad Text del control TextBox. El método WriteTextSafe consulta InvokeRequired. Si InvokeRequired devuelve true, WriteTextSafe se pasa SafeCallDelegate al método Invoke para realizar la llamada real al control. Si InvokeRequired devuelve false, WriteTextSafe se establece TextBox.Text directamente. El controlador de eventos Button1_Click crea el nuevo subproceso y ejecuta el método WriteTextSafe.

using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

public class InvokeThreadSafeForm : Form
{
    private delegate void SafeCallDelegate(string text);
    private Button button1;
    private TextBox textBox1;
    private Thread thread2 = null;

    [STAThread]
    static void Main()
    {
        Application.SetCompatibleTextRenderingDefault(false);
        Application.EnableVisualStyles();
        Application.Run(new InvokeThreadSafeForm());
    }
    public InvokeThreadSafeForm()
    {
        button1 = new Button
        {
            Location = new Point(15, 55),
            Size = new Size(240, 20),
            Text = "Set text safely"
        };
        button1.Click += new EventHandler(Button1_Click);
        textBox1 = new TextBox
        {
            Location = new Point(15, 15),
            Size = new Size(240, 20)
        };
        Controls.Add(button1);
        Controls.Add(textBox1);
    }

    private void Button1_Click(object sender, EventArgs e)
    {
        thread2 = new Thread(new ThreadStart(SetText));
        thread2.Start();
        Thread.Sleep(1000);
    }

    private void WriteTextSafe(string text)
    {
        if (textBox1.InvokeRequired)
        {
            var d = new SafeCallDelegate(WriteTextSafe);
            textBox1.Invoke(d, new object[] { text });
        }
        else
        {
            textBox1.Text = text;
        }
    }

    private void SetText()
    {
        WriteTextSafe("This text was set safely.");
    }
}
Imports System.Drawing
Imports System.Threading
Imports System.Windows.Forms

Public Class InvokeThreadSafeForm : Inherits Form

    Public Shared Sub Main()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.EnableVisualStyles()
        Dim frm As New InvokeThreadSafeForm()
        Application.Run(frm)
    End Sub

    Dim WithEvents Button1 As Button
    Dim TextBox1 As TextBox
    Dim Thread2 as Thread = Nothing

    Delegate Sub SafeCallDelegate(text As String)

    Private Sub New()
        Button1 = New Button()
        With Button1
            .Location = New Point(15, 55)
            .Size = New Size(240, 20)
            .Text = "Set text safely"
        End With
        TextBox1 = New TextBox()
        With TextBox1
            .Location = New Point(15, 15)
            .Size = New Size(240, 20)
        End With
        Controls.Add(Button1)
        Controls.Add(TextBox1)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Thread2 = New Thread(New ThreadStart(AddressOf SetText))
        Thread2.Start()
        Thread.Sleep(1000)
    End Sub

    Private Sub WriteTextSafe(text As String)
        If TextBox1.InvokeRequired Then
            Dim d As New SafeCallDelegate(AddressOf SetText)
            TextBox1.Invoke(d, New Object() {text})
        Else
            TextBox1.Text = text
        End If
    End Sub

    Private Sub SetText()
        WriteTextSafe("This text was set safely.")
    End Sub
End Class

Ejemplo: usar un controlador de eventos BackgroundWorker

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

Para realizar una llamada segura para subprocesos mediante BackgroundWorker, cree un método en el subproceso en segundo plano para realizar el trabajo y enlazarlo al evento DoWork. Cree otro método en el subproceso principal para notificar los resultados del trabajo en segundo plano y enlazarlo a los eventos ProgressChanged o RunWorkerCompleted. Para iniciar el subproceso en segundo plano, llame a BackgroundWorker.RunWorkerAsync.

En el ejemplo se usa el controlador de eventos RunWorkerCompleted para establecer la propiedad Text del control TextBox. Para obtener un ejemplo en el que se usa el evento ProgressChanged, vea BackgroundWorker.

using System;
using System.ComponentModel;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

public class BackgroundWorkerForm : Form
{
    private BackgroundWorker backgroundWorker1;
    private Button button1;
    private TextBox textBox1;

    [STAThread]
    static void Main()
    {
        Application.SetCompatibleTextRenderingDefault(false);
        Application.EnableVisualStyles();
        Application.Run(new BackgroundWorkerForm());
    }
    public BackgroundWorkerForm()
    {
        backgroundWorker1 = new BackgroundWorker();
        backgroundWorker1.DoWork += new DoWorkEventHandler(BackgroundWorker1_DoWork);
        backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BackgroundWorker1_RunWorkerCompleted);
        button1 = new Button
        {
            Location = new Point(15, 55),
            Size = new Size(240, 20),
            Text = "Set text safely with BackgroundWorker"
        };
        button1.Click += new EventHandler(Button1_Click);
        textBox1 = new TextBox
        {
            Location = new Point(15, 15),
            Size = new Size(240, 20)
        };
        Controls.Add(button1);
        Controls.Add(textBox1);
    }
    private void Button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.RunWorkerAsync();
    }

    private void BackgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        // Sleep 2 seconds to emulate getting data.
        Thread.Sleep(2000);
        e.Result = "This text was set safely by BackgroundWorker.";
    }

    private void BackgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        textBox1.Text = e.Result.ToString();
    }
}
Imports System.ComponentModel
Imports System.Drawing
Imports System.Threading
Imports System.Windows.Forms

Public Class BackgroundWorkerForm : Inherits Form

    Public Shared Sub Main()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.EnableVisualStyles()
        Dim frm As New BackgroundWorkerForm()
        Application.Run(frm)
    End Sub

    Dim WithEvents BackgroundWorker1 As BackgroundWorker
    Dim WithEvents Button1 As Button
    Dim TextBox1 As TextBox

    Private Sub New()
        BackgroundWorker1 = New BackgroundWorker()
        Button1 = New Button()
        With Button1
            .Text = "Set text safely with BackgroundWorker"
            .Location = New Point(15, 55)
            .Size = New Size(240, 20)
        End With
        TextBox1 = New TextBox()
        With TextBox1
            .Location = New Point(15, 15)
            .Size = New Size(240, 20)
        End With
        Controls.Add(Button1)
        Controls.Add(TextBox1)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        BackgroundWorker1.RunWorkerAsync()
    End Sub

    Private Sub BackgroundWorker1_DoWork(sender As Object, e As DoWorkEventArgs) _
     Handles BackgroundWorker1.DoWork
        ' Sleep 2 seconds to emulate getting data.
        Thread.Sleep(2000)
        e.Result = "This text was set safely by BackgroundWorker."
    End Sub

    Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) _
     Handles BackgroundWorker1.RunWorkerCompleted
        textBox1.Text = e.Result.ToString()
    End Sub
End Class

Consulte también