Instrukcje: tworzenie wywołań bezpiecznych wątkowo do kontrolek Windows Forms

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 bardzo 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. Możesz użyć System.Windows.Forms.Control.Invoke metody , aby wywołać delegata utworzonego w głównym wątku, który z kolei wywołuje kontrolkę. Możesz też zaimplementować klasę System.ComponentModel.BackgroundWorker, która 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)
{
    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

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 "" uzyskiwana z wątku innego niż wątek, w ramach którego została utworzona. 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.

Sejf wywołania między wątkami

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.

Możesz skompilować i uruchomić te przykłady jako aplikacje .NET Framework z poziomu wiersza polecenia języka C# lub Visual Basic. Aby uzyskać więcej informacji, zobacz Kompilowanie wiersza polecenia za pomocą pliku csc.exe lub Build z wiersza polecenia (Visual Basic).

Począwszy od platformy .NET Core 3.0, można również skompilować i uruchomić przykłady jako aplikacje platformy .NET Core z folderu zawierającego nazwę folderu .NET Core Windows Forms.csproj<.>

Przykład: używanie metody Invoke z pełnomocnikiem

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 identyfikatory wątków są takie same, wywołuje kontrolkę bezpośrednio. Jeśli identyfikatory wątków są inne, wywołuje Control.Invoke metodę z delegatem z wątku głównego, co sprawia, że rzeczywiste wywołanie kontrolki.

Włącza SafeCallDelegate ustawienie TextBox właściwości kontrolki Text . Metoda WriteTextSafe wykonuje zapytanie InvokeRequired. Jeśli InvokeRequired funkcja zwraca truewartość , WriteTextSafe przekazuje SafeCallDelegate metodę Invoke do metody , aby wykonać rzeczywiste wywołanie kontrolki. 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 .

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

Przykład: używanie programu obsługi zdarzeń BackgroundWorker

Łatwym sposobem zaimplementowania wielowątku jest składnik, który korzysta z System.ComponentModel.BackgroundWorker modelu opartego na zdarzeniach. Wątek w tle uruchamia 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 przy użyciu metody BackgroundWorker, utwórz metodę w wątku w tle, aby wykonać pracę i powiązać ją ze zdarzeniem DoWork . Utwórz inną metodę w wątku głównym, aby zgłosić wyniki pracy w tle i powiązać ją ze zdarzeniem ProgressChanged lub RunWorkerCompleted . Aby uruchomić wątek w tle, wywołaj metodę BackgroundWorker.RunWorkerAsync.

W przykładzie użyto RunWorkerCompleted procedury obsługi zdarzeń, aby ustawić TextBox właściwość kontrolki Text . Przykład użycia zdarzenia można znaleźć w ProgressChanged temacie 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

Zobacz też