Postupy: Volání ovládacích prvků model Windows Forms bezpečnými vlákny

Multithreading může zlepšit výkon model Windows Forms aplikací, ale přístup k ovládacím prvkům model Windows Forms není ze své podstaty bezpečný pro přístup z více vláken. Multithreading může váš kód vystavit velmi vážným a složitým chybám. Dvě nebo více vláken manipulující s ovládacím prvek může vynutit řízení do nekonzistentního stavu a vést k podmínkám časování, zablokování a zablokování nebo zablokování. Pokud ve své aplikaci implementujete vícevláknové funkce, nezapomeňte volat ovládací prvky napříč vlákny bezpečným způsobem. Další informace najdete v tématu Osvědčené postupy spravovaných vláken.

Existují dva způsoby, jak bezpečně volat ovládací prvek model Windows Forms z vlákna, které ovládací prvek nevytvořil. Metodu System.Windows.Forms.Control.Invoke můžete použít k volání delegáta vytvořeného v hlavním vlákně, který pak volá ovládací prvek. Nebo můžete implementovat System.ComponentModel.BackgroundWorkermodel řízený událostmi k oddělení práce provedené ve vlákně na pozadí od generování sestav výsledků.

Nebezpečná volání mezi vlákny

Volání ovládacího prvku přímo z vlákna, které ho nevytvořilo, je nebezpečné. Následující fragment kódu znázorňuje nebezpečné volání System.Windows.Forms.TextBox ovládacího prvku. Obslužná rutina Button1_Click události vytvoří nové WriteTextUnsafe vlákno, které nastaví vlastnost hlavního TextBox.Text vlákna přímo.

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

Ladicí program sady Visual Studio detekuje tato nebezpečná volání vlákna vyvoláním InvalidOperationException operace křížového vlákna, která není platná. Řízení "" přístupné z jiného vlákna, než je vlákno, na které bylo vytvořeno. Vždy InvalidOperationException dochází k nebezpečným voláním mezi vlákny během ladění sady Visual Studio a může dojít za běhu aplikace. Problém byste měli vyřešit, ale výjimku můžete zakázat nastavením Control.CheckForIllegalCrossThreadCalls vlastnosti na falsehodnotu .

Sejf volání mezi vlákny

Následující příklady kódu ukazují dva způsoby bezpečného volání ovládacího prvku model Windows Forms z vlákna, které ho nevytvořilo:

  1. Metoda System.Windows.Forms.Control.Invoke , která volá delegáta z hlavního vlákna pro volání ovládacího prvku.
  2. Komponenta System.ComponentModel.BackgroundWorker , která nabízí model řízený událostmi.

V oboupříkladch

Tyto příklady můžete sestavit a spustit jako aplikace rozhraní .NET Framework z příkazového řádku jazyka C# nebo Visual Basic. Další informace naleznete v tématu Vytváření příkazového řádku pomocí csc.exe nebo Sestavení z příkazového řádku (Visual Basic).

Počínaje .NET Core 3.0 můžete také sestavit a spustit příklady jako aplikace windows .NET Core ze složky, která obsahuje soubor projektu .NET Core model Windows Forms <název> složky.csproj.

Příklad: Použití metody Invoke s delegátem

Následující příklad ukazuje vzor pro zajištění volání typu thread-safe do model Windows Forms řízení. Dotazuje se na System.Windows.Forms.Control.InvokeRequired vlastnost, která porovnává ID vlákna vytvářeného ovládacího prvku s ID volajícího vlákna. Pokud jsou ID vláken stejná, volá ovládací prvek přímo. Pokud se ID vláken liší, volá metodu Control.Invoke s delegátem z hlavního vlákna, což provádí skutečné volání ovládacího prvku.

Povolí SafeCallDelegate nastavení TextBox vlastnosti ovládacího prvku Text . Metoda WriteTextSafe dotazuje InvokeRequired. Pokud InvokeRequired vrátí true, WriteTextSafe předá SafeCallDelegate metodě Invoke skutečné volání do ovládacího prvku. Pokud InvokeRequired se vrátí false, WriteTextSafe nastaví přímo TextBox.Text . Obslužná rutina Button1_Click události vytvoří nové vlákno a spustí metodu 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

Příklad: Použití obslužné rutiny události BackgroundWorker

Snadný způsob implementace multithreadingu je s komponentou System.ComponentModel.BackgroundWorker , která používá model řízený událostmi. Vlákno na pozadí spustí BackgroundWorker.DoWork událost, která nepracuje s hlavním vláknem. Hlavní vlákno spouští BackgroundWorker.ProgressChanged obslužné rutiny událostí BackgroundWorker.RunWorkerCompleted , které mohou volat ovládací prvky hlavního vlákna.

Chcete-li provést volání bezpečné pro přístup z více vláken pomocí BackgroundWorker, vytvořte metodu ve vlákně na pozadí k provedení práce a vytvořte vazbu s událostí DoWork . Vytvořte v hlavním vlákně jinou metodu, která bude hlásit výsledky práce na pozadí a vytvořit vazbu s událostí nebo RunWorkerCompleted událostíProgressChanged. Pokud chcete spustit vlákno na pozadí, zavolejte BackgroundWorker.RunWorkerAsync.

Příklad používá obslužnou rutinu RunWorkerCompleted události k nastavení TextBox vlastnosti ovládacího prvku Text . Příklad použití ProgressChanged události naleznete v tématu 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

Viz také