Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
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 button2_Click(object sender, EventArgs e)
{
WriteTextUnsafe("Writing message #1 (UI THREAD)");
_ = Task.Run(() => WriteTextUnsafe("Writing message #2 (OTHER THREAD)"));
}
private void WriteTextUnsafe(string text) =>
textBox1.Text += $"{Environment.NewLine}{text}";
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
WriteTextUnsafe("Writing message #1 (UI THREAD)")
Task.Run(Sub() WriteTextUnsafe("Writing message #2 (OTHER THREAD)"))
End Sub
Private Sub WriteTextUnsafe(text As String)
TextBox1.Text += $"{Environment.NewLine}{text}"
End Sub
El depurador de Visual Studio detecta estas llamadas a subprocesos no seguras mediante la generación de un InvalidOperationException elemento con el mensaje Operación entre subprocesos no válida. Control al que se accede desde un subproceso distinto del subproceso en el que se creó.InvalidOperationException Siempre se produce 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 a través de hilos
Las aplicaciones de Windows Forms siguen un marco similar a un contrato estricto, similar a todos los demás marcos de interfaz de usuario de Windows: se debe crear y acceder a todos los controles desde el mismo subproceso. Esto es importante porque Windows requiere que las aplicaciones proporcionen un único subproceso dedicado para entregar mensajes del sistema. Cada vez que el Administrador de ventanas de Windows detecta una interacción con una ventana de aplicación, como una pulsación de tecla, un clic del mouse o el cambio de tamaño de la ventana, enruta esa información al subproceso que creó y administra la interfaz de usuario y la convierte en eventos accionables. Este subproceso se conoce como subproceso de interfaz de usuario.
Dado que el código que se ejecuta en otro subproceso no puede acceder a los controles creados y administrados por el subproceso de interfaz de usuario, Windows Forms proporciona maneras de trabajar de forma segura con estos controles desde otro subproceso, como se muestra en los ejemplos de código siguientes:
Ejemplo: Usar Control.InvokeAsync (.NET 9 y versiones posteriores)
El Control.InvokeAsync método (.NET 9+), que proporciona serialización asincrónica al subproceso de la interfaz de usuario.
Ejemplo: Use el método Control.Invoke:
Método Control.Invoke, que llama a un delegado desde el hilo principal para invocar al control.
Ejemplo: Usar un backgroundWorker
Un BackgroundWorker componente, que ofrece un modelo controlado por eventos.
Ejemplo: Usar Control.InvokeAsync (.NET 9 y versiones posteriores)
A partir de .NET 9, Windows Forms incluye el InvokeAsync método , que proporciona serialización asincrónica al subproceso de la interfaz de usuario. Este método es útil para los controladores de eventos asincrónicos y elimina muchos escenarios comunes de interbloqueo.
Nota:
Control.InvokeAsync solo está disponible en .NET 9 y versiones posteriores. No se admite en .NET Framework.
Descripción de la diferencia: Invocar frente a InvokeAsync
Control.Invoke (envío - bloqueo):
- Envía de forma sincrónica el delegado a la cola de mensajes del subproceso de la interfaz de usuario.
- El subproceso que llama espera hasta que el subproceso de interfaz de usuario procesa el delegado.
- Puede provocar que la interfaz de usuario se inmoviliza cuando el delegado serializado a la cola de mensajes está esperando a que llegue un mensaje (interbloqueo).
- Resulta útil cuando tiene resultados listos para mostrarse en el subproceso de la interfaz de usuario, por ejemplo: deshabilitar un botón o establecer el texto de un control.
Control.InvokeAsync (Contabilización: no bloqueo):
- Publica de forma asincrónica el delegado en la cola de mensajes del subproceso de la interfaz de usuario en lugar de esperar a que finalice la invocación.
- Devuelve inmediatamente sin bloquear el subproceso que realiza la llamada.
- Devuelve un
Taskobjeto que se puede esperar para completarse. - Ideal para escenarios asincrónicos y evita cuellos de botella de subprocesos de interfaz de usuario.
Ventajas de InvokeAsync
Control.InvokeAsync tiene varias ventajas sobre el método anterior Control.Invoke . Devuelve un Task que puede esperar, lo que hace que funcione bien con código asincrónico y await. También evita problemas comunes de interbloqueo que pueden producirse al mezclar código asincrónico con llamadas de invocación sincrónicas. A diferencia Control.Invokede , el InvokeAsync método no bloquea el subproceso que realiza la llamada, lo que mantiene la capacidad de respuesta de las aplicaciones.
El método admite la cancelación a través CancellationTokende , por lo que puede cancelar las operaciones cuando sea necesario. También controla correctamente las excepciones y las devuelve al código para que pueda tratar correctamente los errores. .NET 9 incluye advertencias del compilador (WFO2001) que le ayudan a usar el método correctamente.
Para obtener instrucciones completas sobre los controladores de eventos asincrónicos y los procedimientos recomendados, consulte Introducción a los eventos.
Elección de la sobrecarga InvokeAsync adecuada
Control.InvokeAsync proporciona cuatro sobrecargas para diferentes escenarios:
| Sobrecarga | Caso de uso | Example |
|---|---|---|
InvokeAsync(Action) |
Operación de sincronización, sin valor devuelto. | Actualizar las propiedades del control. |
InvokeAsync<T>(Func<T>) |
Operación de sincronización, con valor devuelto. | Obtiene el estado del control. |
InvokeAsync(Func<CancellationToken, ValueTask>) |
Operación asincrónica, sin valor devuelto.* | Actualizaciones de la interfaz de usuario de larga duración. |
InvokeAsync<T>(Func<CancellationToken, ValueTask<T>>) |
Operación asincrónica, con valor devuelto.* | Captura de datos asincrónicos con el resultado. |
*Visual Basic no admite la espera de .ValueTask
En el ejemplo siguiente se muestra cómo usar InvokeAsync para actualizar controles de forma segura desde un subproceso en segundo plano:
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
try
{
// Perform background work
await Task.Run(async () =>
{
for (int i = 0; i <= 100; i += 10)
{
// Simulate work
await Task.Delay(100);
// Create local variable to avoid closure issues
int currentProgress = i;
// Update UI safely from background thread
await loggingTextBox.InvokeAsync(() =>
{
loggingTextBox.Text = $"Progress: {currentProgress}%";
});
}
});
loggingTextBox.Text = "Operation completed!";
}
finally
{
button1.Enabled = true;
}
}
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles button1.Click
button1.Enabled = False
Try
' Perform background work
Await Task.Run(Async Function()
For i As Integer = 0 To 100 Step 10
' Simulate work
Await Task.Delay(100)
' Create local variable to avoid closure issues
Dim currentProgress As Integer = i
' Update UI safely from background thread
Await loggingTextBox.InvokeAsync(Sub()
loggingTextBox.Text = $"Progress: {currentProgress}%"
End Sub)
Next
End Function)
' Update UI after completion
Await loggingTextBox.InvokeAsync(Sub()
loggingTextBox.Text = "Operation completed!"
End Sub)
Finally
button1.Enabled = True
End Try
End Sub
Para las operaciones asincrónicas que deben ejecutarse en el subproceso de la interfaz de usuario, use la sobrecarga asincrónica:
private async void button2_Click(object sender, EventArgs e)
{
button2.Enabled = false;
try
{
loggingTextBox.Text = "Starting operation...";
// Dispatch and run on a new thread, but wait for tasks to finish
// Exceptions are rethrown here, because await is used
await Task.WhenAll(Task.Run(SomeApiCallAsync),
Task.Run(SomeApiCallAsync),
Task.Run(SomeApiCallAsync));
// Dispatch and run on a new thread, but don't wait for task to finish
// Exceptions are not rethrown here, because await is not used
_ = Task.Run(SomeApiCallAsync);
}
catch (OperationCanceledException)
{
loggingTextBox.Text += "Operation canceled.";
}
catch (Exception ex)
{
loggingTextBox.Text += $"Error: {ex.Message}";
}
finally
{
button2.Enabled = true;
}
}
private async Task SomeApiCallAsync()
{
using var client = new HttpClient();
// Simulate random network delay
await Task.Delay(Random.Shared.Next(500, 2500));
// Do I/O asynchronously
string result = await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md");
// Marshal back to UI thread
await this.InvokeAsync(async (cancelToken) =>
{
loggingTextBox.Text += $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}";
});
// Do more async I/O ...
}
Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles button2.Click
button2.Enabled = False
Try
loggingTextBox.Text = "Starting operation..."
' Dispatch and run on a new thread, but wait for tasks to finish
' Exceptions are rethrown here, because await is used
Await Task.WhenAll(Task.Run(AddressOf SomeApiCallAsync),
Task.Run(AddressOf SomeApiCallAsync),
Task.Run(AddressOf SomeApiCallAsync))
' Dispatch and run on a new thread, but don't wait for task to finish
' Exceptions are not rethrown here, because await is not used
Call Task.Run(AddressOf SomeApiCallAsync)
Catch ex As OperationCanceledException
loggingTextBox.Text += "Operation canceled."
Catch ex As Exception
loggingTextBox.Text += $"Error: {ex.Message}"
Finally
button2.Enabled = True
End Try
End Sub
Private Async Function SomeApiCallAsync() As Task
Using client As New HttpClient()
' Simulate random network delay
Await Task.Delay(Random.Shared.Next(500, 2500))
' Do I/O asynchronously
Dim result As String = Await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md")
' Marshal back to UI thread
' Extra work here in VB to handle ValueTask conversion
Await Me.InvokeAsync(DirectCast(
Async Function(cancelToken As CancellationToken) As Task
loggingTextBox.Text &= $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}"
End Function,
Func(Of CancellationToken, Task)).AsValueTask() 'Extension method to convert Task
)
' Do more Async I/O ...
End Using
End Function
Nota:
Si usa Visual Basic, el fragmento de código anterior usó un método de extensión para convertir en ValueTask .Task El código del método de extensión está disponible en GitHub.
Ejemplo: Usar el método Control.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)
{
WriteTextSafe("Writing message #1");
_ = Task.Run(() => WriteTextSafe("Writing message #2"));
}
public void WriteTextSafe(string text)
{
if (textBox1.InvokeRequired)
textBox1.Invoke(() => WriteTextSafe($"{text} (NON-UI THREAD)"));
else
textBox1.Text += $"{Environment.NewLine}{text}";
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
WriteTextSafe("Writing message #1")
Task.Run(Sub() WriteTextSafe("Writing message #2"))
End Sub
Private Sub WriteTextSafe(text As String)
If (TextBox1.InvokeRequired) Then
TextBox1.Invoke(Sub()
WriteTextSafe($"{text} (NON-UI THREAD)")
End Sub)
Else
TextBox1.Text += $"{Environment.NewLine}{text}"
End If
End Sub
Para obtener más información sobre cómo Invoke difiere de InvokeAsync, vea Descripción de la diferencia: Invocar frente a InvokeAsync.
Ejemplo: Usar un BackgroundWorker
Una manera fácil de implementar escenarios de varios subprocesos, al tiempo que garantiza que el acceso a un control o formulario solo se realiza en el subproceso principal (subproceso de interfaz de usuario), 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.
Importante
El BackgroundWorker componente ya no es el enfoque recomendado para escenarios asincrónicos en aplicaciones de Windows Forms. Aunque seguimos admitiendo este componente para la compatibilidad con versiones anteriores, solo se dirige a la descarga de la carga de trabajo del procesador del subproceso de la interfaz de usuario a otro subproceso. No controla otros escenarios asincrónicos, como la E/S de archivos o las operaciones de red en las que es posible que el procesador no funcione activamente.
Para la programación asincrónica moderna, use async métodos con await en su lugar. Si necesita descargar explícitamente el trabajo intensivo del procesador, use Task.Run para crear e iniciar una nueva tarea, que puede esperar como cualquier otra operación asincrónica. Para obtener más información, vea Ejemplo: Usar Control.InvokeAsync (.NET 9 y versiones posteriores) yoperaciones y eventos entre subprocesos.
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 ProgressChanged evento se usa para comunicar las actualizaciones de estado al subproceso principal y el RunWorkerCompleted evento se usa para indicar que el trabajo en segundo plano se ha completado. 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(); // Not awaitable
}
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() ' Not awaitable
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