Udostępnij za pośrednictwem


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 narazić twój kod na bardzo poważne i złożone błędy. Dwa lub więcej wątków manipulujących kontrolką mogą wprowadzić ją w niespójny stan, co może prowadzić do warunków wyścigu, zakleszczeń oraz zawieszeń. W przypadku implementowania wielowątkowości w aplikacji należy wywołać kontrolki międzywątkowe w sposób bezpieczny dla wątków. Aby uzyskać więcej informacji, zobacz Najlepsze praktyki zarządzania wątkami.

Istnieją dwa sposoby bezpiecznego wywoływania kontrolki Windows Forms z wątku, który nie utworzył tej kontrolki. Za pomocą metody System.Windows.Forms.Control.Invoke można wywołać delegata utworzonego w wątku głównym, który z kolei wywołuje kontrolkę. Możesz też zaimplementować System.ComponentModel.BackgroundWorker, która używa modelu opartego na zdarzeniach w celu oddzielenia pracy wykonywanej w wątku w tle od raportowania wyników.

Niebezpieczne wywołania międzywątkowe

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

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 z komunikatem, Nieprawidłowa operacja między wątkami. Kontrolka "" została uzyskana z wątku innego niż ten, na którym została utworzona.InvalidOperationException zawsze występuje w przypadku niebezpiecznych wywołań między wątkami podczas debugowania w programie Visual Studio i może wystąpić w czasie wykonywania aplikacji. Należy rozwiązać ten problem, ale można wyłączyć wyjątek, ustawiając właściwość Control.CheckForIllegalCrossThreadCalls 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. Składnik System.ComponentModel.BackgroundWorker, 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 przy użyciu 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 z nazwą folderu .NET Core Windows Forms <>.csproj pliku projektu.

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 dla właściwości System.Windows.Forms.Control.InvokeRequired, 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 metodę Control.Invoke z delegatem z wątku głównego, co powoduje rzeczywiste wywołanie kontrolki.

SafeCallDelegate umożliwia ustawienie właściwości Text kontrolki TextBox. Metoda WriteTextSafe wykonuje zapytania InvokeRequired. Jeśli InvokeRequired zwraca true, WriteTextSafe przekazuje SafeCallDelegate do metody Invoke, aby wykonać rzeczywiste wywołanie kontrolki. Jeśli InvokeRequired zwraca false, WriteTextSafe ustawia TextBox.Text bezpośrednio. Program obsługi zdarzeń Button1_Click 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 użycie składnika System.ComponentModel.BackgroundWorker, który korzysta z modelu opartego na zdarzeniach. Wątek w tle uruchamia zdarzenie BackgroundWorker.DoWork, które nie wchodzi w interakcję z głównym wątkiem. Główny wątek uruchamia programy obsługi zdarzeń BackgroundWorker.ProgressChanged i BackgroundWorker.RunWorkerCompleted, które mogą wywoływać kontrolki głównego wątku.

Aby utworzyć bezpieczne wątkowo wywołanie przy użyciu 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 działający w tle, wywołaj BackgroundWorker.RunWorkerAsync.

W przykładzie użyto procedury obsługi zdarzeń RunWorkerCompleted, aby ustawić właściwość Text kontrolki TextBox. Aby zapoznać się z przykładem użycia zdarzenia ProgressChanged, zobacz 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ż