Как выполнять потокобезопасные вызовы элементов управления (Windows Forms .NET)

Многопоточность может повысить производительность приложений Windows Forms, но доступ к элементам управления Windows Forms сам по себе не является потокобезопасным. Многопоточность может предоставлять код серьезным и сложным ошибкам. Если элементом управления управляет больше одного потока, элемент управления может перейти в несогласованное состояние, что приведет к состоянию гонки, взаимоблокировкам и зависаниям. Если вы реализуете многопоточность в приложении, вызывайте элементы управления в разных потоках потокобезопасным способом. Дополнительные сведения см. в рекомендациях по управлению потоками.

Важно!

Документация по рабочему столу для .NET 7 и .NET 6 находится в стадии разработки.

Существует два способа безопасного вызова элемента управления 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)
{
    var thread2 = new System.Threading.Thread(WriteTextUnsafe);
    thread2.Start();
}

private void WriteTextUnsafe() =>
    textBox1.Text = "This text was set unsafely.";
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim thread2 As New System.Threading.Thread(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, который предлагает модель на основе событий.

В обоих примерах фоновый поток переходит в спящий режим на одну секунду, чтобы имитировать работу, выполняемую в этом потоке.

Пример. Использование метода 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)
{
    var threadParameters = new System.Threading.ThreadStart(delegate { WriteTextSafe("This text was set safely."); });
    var thread2 = new System.Threading.Thread(threadParameters);
    thread2.Start();
}

public void WriteTextSafe(string text)
{
    if (textBox1.InvokeRequired)
    {
        // Call this same method but append THREAD2 to the text
        Action safeWrite = delegate { WriteTextSafe($"{text} (THREAD2)"); };
        textBox1.Invoke(safeWrite);
    }
    else
        textBox1.Text = text;
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Dim threadParameters As New System.Threading.ThreadStart(Sub()
                                                                 WriteTextSafe("This text was set safely.")
                                                             End Sub)

    Dim thread2 As New System.Threading.Thread(threadParameters)
    thread2.Start()

End Sub

Private Sub WriteTextSafe(text As String)

    If (TextBox1.InvokeRequired) Then

        TextBox1.Invoke(Sub()
                            WriteTextSafe($"{text} (THREAD2)")
                        End Sub)

    Else
        TextBox1.Text = text
    End If

End Sub

Пример. Использование BackgroundWorker

Простой способ реализовать многопоточность — использовать компонент System.ComponentModel.BackgroundWorker с моделью на основе событий. Фоновый поток вызывает BackgroundWorker.DoWork событие, которое не взаимодействует с основным потоком. Основной поток запускает обработчики событий BackgroundWorker.ProgressChanged и BackgroundWorker.RunWorkerCompleted, которые могут вызывать элементы управления основного потока.

Чтобы сделать потокобезопасный вызов с помощью 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();
}

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()
    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