Jak wykonywać bezpieczne wątkowo wywołania kontrolek (Windows Forms .NET)

Wielowątkowość może zwiększyć wydajność aplikacji Windows Forms, ale dostęp do kontrolek Windows Forms nie jest z natury bezpieczny wątkowo. Wielowątkowość może uwidocznić kod na poważne i złożone błędy. Co najmniej dwa wątki manipulujące kontrolką mogą wymusić kontrolę na niespójny stan i prowadzić do warunków wyścigu, zakleszczeń i zawiesza się lub zawiesza. W przypadku implementowania wielowątków w aplikacji należy wywołać kontrolki międzywątkowa w bezpieczny wątkowo sposób. Aby uzyskać więcej informacji, zobacz Managed threading best practices (Najlepsze rozwiązania dotyczące zarządzanych wątków).

Istnieją dwa sposoby bezpiecznego wywoływania kontrolki Windows Forms z wątku, który nie utworzył tej kontrolki. System.Windows.Forms.Control.Invoke Użyj metody , aby wywołać delegata utworzonego w wątku głównym, który z kolei wywołuje kontrolkę. Lub zaimplementuj System.ComponentModel.BackgroundWorkerelement , który używa modelu opartego na zdarzeniach, aby oddzielić pracę wykonaną w wątku w tle od raportowania wyników.

Niebezpieczne wywołania międzywątowe

Nie można wywołać kontrolki bezpośrednio z wątku, który go nie utworzył. Poniższy fragment kodu ilustruje niebezpieczne wywołanie kontrolki System.Windows.Forms.TextBox . Procedura Button1_Click obsługi zdarzeń tworzy nowy WriteTextUnsafe wątek, który ustawia właściwość głównego wątku TextBox.Text bezpośrednio.

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

Debuger programu Visual Studio wykrywa te niebezpieczne wywołania wątków, wywołując InvalidOperationException komunikat z nieprawidłową operacją między wątkami. Kontrola dostępu z wątku innego niż wątek, na który został utworzony. Zawsze występuje w InvalidOperationException przypadku niebezpiecznych wywołań między wątkami podczas debugowania programu Visual Studio i może wystąpić w środowisku uruchomieniowym aplikacji. Należy rozwiązać ten problem, ale można wyłączyć wyjątek, ustawiając Control.CheckForIllegalCrossThreadCalls właściwość na false.

Bezpieczne wywołania międzywątowe

W poniższych przykładach kodu pokazano dwa sposoby bezpiecznego wywoływania kontrolki Formularze systemu Windows z wątku, który go nie utworzył:

  1. Metoda System.Windows.Forms.Control.Invoke , która wywołuje delegata z głównego wątku w celu wywołania kontrolki.
  2. System.ComponentModel.BackgroundWorker Składnik, który oferuje model oparty na zdarzeniach.

W obu przykładach wątek w tle jest w stanie uśpienia przez jedną sekundę w celu symulowania pracy wykonywanej w tym wątku.

Przykład: użyj metody Invoke

W poniższym przykładzie pokazano wzorzec zapewniający bezpieczne wątkowo wywołania kontrolki Windows Forms. Wykonuje zapytanie względem System.Windows.Forms.Control.InvokeRequired właściwości, która porównuje identyfikator wątku tworzenia kontrolki z identyfikatorem wywołującego wątku. Jeśli są różne, należy wywołać metodę Control.Invoke .

Właściwość WriteTextSafe umożliwia ustawienie TextBox właściwości kontrolki Text na nową wartość. Metoda wykonuje zapytanie InvokeRequired. Jeśli InvokeRequired zwraca wartość true, WriteTextSafe rekursywnie wywołuje samą siebie, przekazując metodę jako delegata do Invoke metody. Jeśli InvokeRequired funkcja zwraca falsewartość , WriteTextSafe ustawia TextBox.Text wartość bezpośrednio. Procedura Button1_Click obsługi zdarzeń tworzy nowy wątek i uruchamia metodę 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

Przykład: używanie narzędzia BackgroundWorker

Łatwym sposobem zaimplementowania wielowątku jest składnik, który korzysta z System.ComponentModel.BackgroundWorker modelu opartego na zdarzeniach. Wątek w tle wywołuje BackgroundWorker.DoWork zdarzenie, które nie wchodzi w interakcję z głównym wątkiem. Główny wątek uruchamia BackgroundWorker.ProgressChanged programy obsługi zdarzeń i BackgroundWorker.RunWorkerCompleted , które mogą wywoływać kontrolki głównego wątku.

Aby utworzyć bezpieczne wątkowo wywołanie za pomocą metody BackgroundWorker, obsłuż DoWork zdarzenie. Istnieją dwa zdarzenia używane przez proces roboczy w tle do raportowania stanu: ProgressChanged i RunWorkerCompleted. Zdarzenie ProgressChanged jest używane do przekazywania aktualizacji stanu do głównego wątku, a RunWorkerCompleted zdarzenie jest używane do sygnalizatora, że proces roboczy w tle zakończył pracę. Aby uruchomić wątek w tle, wywołaj metodę BackgroundWorker.RunWorkerAsync.

Przykład zlicza od 0 do 10 w DoWork zdarzeniu, wstrzymując wstrzymując jedną sekundę między liczbami. Używa ProgressChanged programu obsługi zdarzeń, aby zgłosić liczbę z powrotem do głównego wątku i ustawić TextBox właściwość kontrolki Text . ProgressChanged Aby zdarzenie działało, właściwość musi być ustawiona BackgroundWorker.WorkerReportsProgress na 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