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

  1. Метод System.Windows.Forms.Control.Invoke, который вызывает делегат из основного потока для вызова элемента управления.
  2. Компонент 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

См. также