Cómo realizar llamadas seguras para subprocesos a controles (Windows Forms .NET)
El multithreading puede mejorar el rendimiento de Windows Forms, pero el acceso a los controles de Windows Forms no es intrínsecamente seguro para subprocesos. Multithreading puede exponer el código a errores 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. Use 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. 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 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 nuevo WriteTextUnsafe
que establece directamente la propiedad del subproceso principal TextBox.Text.
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 no seguras a subprocesos mediante la generación de un InvalidOperationException elemento con el mensaje Operación entre subprocesos no válida. El control se accede desde un subproceso distinto del subproceso en el que se ha creado. El InvalidOperationException se produce siempre para las llamadas entre subprocesos no seguras durante la depuración de Visual Studio y pueden producirse en 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:
- El método System.Windows.Forms.Control.Invoke, que llama a un delegado del subproceso principal para llamar al control.
- 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.
Ejemplo: usar el 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 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 son diferentes, debe llamar al método Control.Invoke.
El WriteTextSafe
habilita el establecimiento de la propiedad Text del control TextBox en un nuevo valor. El método consulta InvokeRequired. Si InvokeRequired devuelve true
, WriteTextSafe
se llama de forma recursiva a sí mismo, pasando el método como delegado al método Invoke. Si InvokeRequired devuelve false
, WriteTextSafe
establece el TextBox.Text directamente. El controlador de eventos Button1_Click
crea el nuevo subproceso y ejecuta el método WriteTextSafe
.
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: uso de un 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 genera 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, controle el evento DoWork. Hay dos eventos que usa el proceso de trabajo en segundo plano para notificar el estado: ProgressChanged y RunWorkerCompleted. El evento ProgressChanged
se usa para comunicar las actualizaciones de estado al subproceso principal y el evento RunWorkerCompleted
se usa para indicar que el proceso de trabajo en segundo plano ha completado su trabajo. Para iniciar el subproceso en segundo plano, llame a BackgroundWorker.RunWorkerAsync.
El ejemplo cuenta de 0 a 10 en el evento DoWork
, pausando durante un segundo entre recuentos. Usa el controlador de eventos ProgressChanged para notificar el número al subproceso principal y establecer la Text propiedad del control TextBox. Para que funcione el evento ProgressChanged, 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
.NET Desktop feedback