Поделиться через


Как выполнять потокобезопасные вызовы к элементам управления

Многопоточность может повысить производительность приложений 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)
{
    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