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:
- 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 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é
.NET Desktop feedback