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 false
hodnotu .
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:
- Metoda System.Windows.Forms.Control.Invoke , která volá delegáta z hlavního vlákna pro volání ovládacího prvku.
- 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é
.NET Desktop feedback