Как создавать потокобезопасные вызовы к элементам управления Windows Forms
Многопоточность может повысить производительность приложений 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 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
Отладчик Visual Studio обнаруживает эти небезопасные вызовы потоков, вызывая InvalidOperationException с сообщением Кросспотоковая операция недопустима. Элемент управления доступен из потока, отличного от того, в котором он был создан.InvalidOperationException всегда возникает для небезопасных вызовов между потоками во время отладки Visual Studio и может возникать во время выполнения программы. Необходимо устранить проблему, но вы можете отключить исключение, задав для свойства Control.CheckForIllegalCrossThreadCalls значение false
.
Безопасные вызовы между потоками
В следующих примерах кода демонстрируется два способа безопасного вызова элемента управления Windows Forms из потока, который не создавал его:
- Метод System.Windows.Forms.Control.Invoke, который вызывает делегат из основного потока для вызова элемента управления.
- Компонент System.ComponentModel.BackgroundWorker, который предлагает модель на основе событий.
В обоих примерах фоновый поток спит на одну секунду, чтобы имитировать работу, выполняемую в этом потоке.
Эти примеры можно создавать и запускать как приложения .NET Framework из командной строки C# или Visual Basic. Дополнительные сведения см. в статье сборка командной строки с csc.exe или сборкой из командной строки (Visual Basic).
Начиная с .NET Core 3.0, вы также можете создавать и запускать примеры как приложения Windows .NET Core из папки с именем .NET Core Windows Forms <>, содержащей файл проекта .csproj.
Пример. Использование метода Invoke с делегатом
Следующий образец демонстрирует способ гарантирования потокобезопасных вызовов элемента управления Windows Forms. Он запрашивает свойство System.Windows.Forms.Control.InvokeRequired, которое сравнивает идентификатор потока создания элемента управления с идентификатором вызывающего потока. Если идентификаторы потоков одинаковы, он вызывает элемент управления напрямую. Если идентификаторы потоков отличаются, он вызывает метод Control.Invoke с делегатом из основного потока, который осуществляет фактический вызов элемента управления.
SafeCallDelegate
позволяет настроить свойство Text элемента управления TextBox. Метод WriteTextSafe
запрашивает InvokeRequired. Если InvokeRequired возвращает true
, WriteTextSafe
передает SafeCallDelegate
методу Invoke, чтобы выполнить фактический вызов элемента управления. Если InvokeRequired возвращает false
, WriteTextSafe
задает TextBox.Text напрямую. Обработчик событий Button1_Click
создает новый поток и запускает метод 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
Пример. Использование обработчика событий BackgroundWorker
Простой способ реализации многопоточных операций — с компонентом System.ComponentModel.BackgroundWorker, который использует модель на основе событий. Фоновый поток запускает событие BackgroundWorker.DoWork, которое не взаимодействует с основным потоком. Основной поток запускает обработчики событий BackgroundWorker.ProgressChanged и BackgroundWorker.RunWorkerCompleted, которые могут вызывать элементы управления основного потока.
Чтобы сделать потокобезопасный вызов с помощью BackgroundWorker, создайте метод в фоновом потоке для выполнения работы и привязать его к событию DoWork. Создайте другой метод в основном потоке, чтобы сообщить о результатах фоновой работы и привязать его к событию ProgressChanged или RunWorkerCompleted. Чтобы запустить фоновый поток, вызовите BackgroundWorker.RunWorkerAsync.
В примере используется обработчик событий RunWorkerCompleted для задания свойства Text элемента управления TextBox. Пример использования события ProgressChanged см. в разделе 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
См. также
.NET Desktop feedback