Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Многопоточность может повысить производительность приложений Windows Forms, но доступ к элементам управления Windows Forms не является изначально потокобезопасным. Многопоточность может подвергать ваш код серьезным и сложным ошибкам. Два или более потоков, управляющих элементом управления, могут привести элемент управления в несогласованное состояние и привести к условиям гонки, взаимоблокировкам и зависаниям. Если вы реализуете многопоточность в приложении, обязательно вызовите элементы управления между потоками в потокобезопасном способе. Для получения дополнительной информации см. лучшие практики по управлению потоками.
Существует два способа безопасного вызова элемента управления Windows Forms из потока, который не создавал этот элемент управления. Используйте метод System.Windows.Forms.Control.Invoke для вызова делегата, созданного в основном потоке, который, в свою очередь, вызывает элемент управления. Или реализуйте System.ComponentModel.BackgroundWorker, которое использует модель, основанную на событиях, чтобы разграничить выполнение работы в фоновом потоке и отчетность о результатах.
Небезопасные вызовы между потоками
Небезопасно вызывать элемент управления напрямую из потока, который его не создавал. В следующем фрагменте кода показан небезопасный вызов элемента управления System.Windows.Forms.TextBox. Обработчик событий Button1_Click создает новый поток WriteTextUnsafe, который задает свойство TextBox.Text основного потока напрямую.
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
Отладчик Visual Studio обнаруживает эти небезопасные вызовы потоков путем вызова сообщения, недопустимой InvalidOperationExceptionмежпотоковой операции. Управление доступом из потока, отличного от потока, на который он был создан. Всегда InvalidOperationException происходит для небезопасных вызовов между потоками во время отладки Visual Studio и может возникать во время выполнения приложения. Необходимо устранить проблему, но вы можете отключить исключение, задав для свойства Control.CheckForIllegalCrossThreadCalls значение false.
Безопасные вызовы между потоками
Приложения Windows Forms соответствуют строгой платформе, аналогичной всем другим платформам пользовательского интерфейса Windows: все элементы управления должны быть созданы и доступны из одного потока. Это важно, так как Windows требует, чтобы приложения предоставляли один выделенный поток для доставки системных сообщений. Всякий раз, когда диспетчер окон Windows обнаруживает взаимодействие с окном приложения, например нажатием клавиши, щелчком мыши или изменением размера окна, он направляет эти сведения в поток, созданный и управляемый пользовательским интерфейсом, и превращает его в события, доступные для действий. Этот поток называется потоком пользовательского интерфейса.
Так как код, запущенный в другом потоке, не может управлять элементами управления доступом, созданными и управляемыми потоком пользовательского интерфейса, Windows Forms предоставляет способы безопасной работы с этими элементами управления из другого потока, как показано в следующих примерах кода:
Пример. Использование Control.InvokeAsync (.NET 9 и более поздних версий)
Метод Control.InvokeAsync (.NET 9+), который обеспечивает асинхронное маршалинг в поток пользовательского интерфейса.
Пример: используйте метод Control.Invoke:
Метод Control.Invoke, который вызывает делегат из основного потока для вызова элемента управления.
Пример. Использование BackgroundWorker
Компонент BackgroundWorker, который предлагает событийно-ориентированную модель.
Пример. Использование Control.InvokeAsync (.NET 9 и более поздних версий)
Начиная с .NET 9, Windows Forms включает InvokeAsync метод, который обеспечивает асинхронное маршалинг в поток пользовательского интерфейса. Этот метод полезен для асинхронных обработчиков событий и устраняет множество распространенных сценариев взаимоблокировки.
Замечание
Control.InvokeAsync доступен только в .NET 9 и более поздних версиях. Он не поддерживается в .NET Framework.
Понимание разницы: Invoke vs InvokeAsync
Control.Invoke (отправка — блокировка):
- Синхронно отправляет делегат в очередь сообщений потока пользовательского интерфейса.
- Вызывающий поток ожидает, пока поток пользовательского интерфейса не обрабатывает делегат.
- Может привести к зависаю пользовательского интерфейса, когда делегат маршалировался в очередь сообщений, ожидает прибытия сообщения (взаимоблокировка).
- Полезно, если результаты готовы отображаться в потоке пользовательского интерфейса, например отключение кнопки или настройка текста элемента управления.
Control.InvokeAsync (публикация — неблокировка):
- Асинхронно отправляет делегат в очередь сообщений потока пользовательского интерфейса вместо ожидания завершения вызова.
- Возвращается немедленно, не блокируя вызывающий поток.
- Возвращает значение
Task, которое можно ожидать завершения. - Идеально подходит для асинхронных сценариев и предотвращает узкие места потока пользовательского интерфейса.
Преимущества InvokeAsync
Control.InvokeAsync Имеет несколько преимуществ по сравнению со старым Control.Invoke методом. Он возвращает Task значение, которое можно ожидать, что оно хорошо работает с асинхронным и ожидаемым кодом. Это также предотвращает распространенные проблемы взаимоблокировки, которые могут возникать при перемешивание асинхронного кода с синхронными вызовами. В отличие от Control.Invokeметода, InvokeAsync метод не блокирует вызывающий поток, который позволяет приложениям реагировать.
Метод поддерживает отмену с помощью CancellationToken, поэтому при необходимости можно отменить операции. Кроме того, он обрабатывает исключения правильно, передавая их обратно в код, чтобы можно было соответствующим образом справиться с ошибками. .NET 9 содержит предупреждения компилятора (WFO2001), которые помогают правильно использовать метод.
Подробные рекомендации по асинхронным обработчикам событий и рекомендациям см. в обзоре событий.
Выбор правильной перегрузки InvokeAsync
Control.InvokeAsync предоставляет четыре перегрузки для разных сценариев:
| Перегрузка | Вариант использования | Example |
|---|---|---|
InvokeAsync(Action) |
Операция синхронизации, возвращаемое значение не возвращается. | Обновите свойства элемента управления. |
InvokeAsync<T>(Func<T>) |
Операция синхронизации с возвращаемым значением. | Получение состояния элемента управления. |
InvokeAsync(Func<CancellationToken, ValueTask>) |
Асинхронная операция без возвращаемого значения.* | Длительные обновления пользовательского интерфейса. |
InvokeAsync<T>(Func<CancellationToken, ValueTask<T>>) |
Асинхронная операция с возвращаемым значением.* | Асинхронное получение данных с результатом. |
*Visual Basic не поддерживает ожидание ValueTask.
В следующем примере показано, как InvokeAsync безопасно обновлять элементы управления из фонового потока:
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
Для асинхронных операций, которые должны выполняться в потоке пользовательского интерфейса, используйте асинхронную перегрузку:
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
Замечание
Если вы используете Visual Basic, предыдущий фрагмент кода использовал метод расширения для преобразования ValueTask в объект Task. Код метода расширения доступен на сайте GitHub.
Пример. Использование метода Control.Invoke
В следующем примере показан шаблон для обеспечения потокобезопасных вызовов контрола Windows Forms. Он запрашивает свойство System.Windows.Forms.Control.InvokeRequired, которое сравнивает идентификатор создающего потока элемента управления с идентификатором вызывающего потока. Если они отличаются, следует вызвать метод Control.Invoke.
WriteTextSafe позволяет задать новому значению свойство TextBox элемента управления Text. Метод запрашивает InvokeRequired. Если InvokeRequired возвращает true, WriteTextSafe рекурсивно вызывает сам метод, передав метод в качестве делегата в метод Invoke. Если InvokeRequired возвращает false, WriteTextSafe задает TextBox.Text напрямую. Обработчик событий Button1_Click создает новый поток и запускает метод WriteTextSafe.
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
Дополнительные сведения о том, как Invoke отличается от InvokeAsync, см. в разделе "Общие сведения о разнице: вызов и вызов" и "InvokeAsync".
Пример: Использование BackgroundWorker
Простой способ реализации многопоточных сценариев, гарантируя, что доступ к элементу управления или форме выполняется только в основном потоке (потоке пользовательского интерфейса), является System.ComponentModel.BackgroundWorker компонентом, использующим модель на основе событий. Фоновый поток вызывает событие BackgroundWorker.DoWork, которое не взаимодействует с основным потоком. Основной поток запускает обработчики событий BackgroundWorker.ProgressChanged и BackgroundWorker.RunWorkerCompleted, которые могут вызывать элементы управления основного потока.
Это важно
Компонент BackgroundWorker больше не рекомендуется использовать для асинхронных сценариев в приложениях Windows Forms. Хотя мы продолжаем поддерживать этот компонент для обратной совместимости, он предназначен только для разгрузки рабочей нагрузки процессора из потока пользовательского интерфейса в другой поток. Он не обрабатывает другие асинхронные сценарии, такие как операции ввода-вывода файлов или сетевых операций, в которых процессор не может активно работать.
Для современного асинхронного программирования используйте async вместо этого методы await . Если необходимо явно выгрузить трудоемкие процессоры, используйте Task.Run для создания и запуска новой задачи, которую можно ожидать, как любая другая асинхронная операция. Дополнительные сведения см. в примере: использование Control.InvokeAsync (.NET 9 и более поздних версий) имежпоточных операций и событий.
Чтобы сделать потокобезопасный вызов с помощью BackgroundWorker, обработайте событие DoWork. Существует два события, которые фоновый работник использует для передачи статуса: ProgressChanged и RunWorkerCompleted. Событие ProgressChanged используется для передачи обновлений состояния основному потоку, и RunWorkerCompleted событие используется для сигнала о завершении фоновой рабочей роли. Чтобы запустить фоновый поток, вызовите BackgroundWorker.RunWorkerAsync.
Пример содержит подсчет от 0 до 10 в событии DoWork, с паузой в одну секунду между каждым числом. В нем используется обработчик событий ProgressChanged, чтобы сообщить о номере обратно в основной поток и установить свойство TextBox для элемента управления Text. Чтобы событие ProgressChanged работало, свойство BackgroundWorker.WorkerReportsProgress должно иметь значение 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