Sdílet prostřednictvím


Postupy: Volání vláknově bezpečných ovládacích prvků Windows Forms

Multithreading může zlepšit výkon aplikací Windows Forms, ale přístup k ovládacím prvkům Windows Forms není přirozeně bezpečný pro práci s více vlákny. 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ích s ovládacím prvkem může vynutit ovládací prvek do nekonzistentního stavu a vést k závodním podmínkám, zablokování a zaseknutí. 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 bezpečného volání ovládacího prvku 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.BackgroundWorker, který používá model řízený událostmi k oddělení práce provedené ve vlákně na pozadí od oznamování 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í ovládacího prvku System.Windows.Forms.TextBox. Obslužná rutina události Button1_Click vytvoří nové vlákno WriteTextUnsafe, které nastaví vlastnost hlavního vlákna TextBox.Text 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áken vyvoláním InvalidOperationException se zprávou: Operace mezi vlákny není platná. Ovládací prvek "" byl přístupný z jiného vlákna, než ve kterém byl vytvořen.InvalidOperationException vždy se vyskytuje u nebezpečných volání mezi vlákny během ladění ve Visual Studio, a může se vyskytnout i při spuštění aplikace. Problém byste měli vyřešit, ale výjimku můžete zakázat nastavením vlastnosti Control.CheckForIllegalCrossThreadCalls na false.

Bezpečná 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 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 obou příkladech běží na pozadí vlákno, které na jednu sekundu usne, aby simulovalo práci prováděnou v tom vlákně.

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 najdete v tématu Sestavení příkazového řádku pomocí csc.exe nebo Sestavení z příkazového řádku (Visual Basic).

Od verze .NET Core 3.0 můžete také sestavit a spustit příklady jako aplikace windows .NET Core ze složky, která má název složky Windows Forms pro .NET Core <>souboru projektu .csproj.

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

Následující příklad ukazuje vzor pro zajištění bezpečných volání k ovládacímu prvku Windows Forms ve více vláknech. Dotazuje se na vlastnost System.Windows.Forms.Control.InvokeRequired, která porovnává ID vlákna, ve kterém byl ovládací prvek vytvořen, 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 delegátem z hlavního vlákna, což provádí skutečné volání ovládacího prvku.

SafeCallDelegate povolí nastavení vlastnosti Text ovládacího prvku TextBox. Metoda WriteTextSafe dotazuje InvokeRequired. Pokud InvokeRequired vrátí true, WriteTextSafe předá SafeCallDelegate metodě Invoke k provedení skutečného volání ovládacího prvku. Pokud InvokeRequired vrátí false, WriteTextSafe nastaví TextBox.Text přímo. Obslužná rutina události Button1_Click 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í spouští událost BackgroundWorker.DoWork, která neinteraguje s hlavním vláknem. Hlavní vlákno spouští obslužné programy událostí BackgroundWorker.ProgressChanged a BackgroundWorker.RunWorkerCompleted, které mohou volat na 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 ve vlákně na pozadí metodu, která provede práci, a navázat ji na událost DoWork. Vytvořte v hlavním vlákně další metodu pro hlášení výsledků práce na pozadí a navázat ji na událost ProgressChanged nebo RunWorkerCompleted. Pokud chcete spustit vlákno na pozadí, zavolejte BackgroundWorker.RunWorkerAsync.

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