Практическое руководство. Осуществление потокобезопасных вызовов элементов управления 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