Anvisningar: Göra trådsäkra anrop till Windows Forms-kontroller
Multitrådning kan förbättra prestandan för Windows Forms-appar, men åtkomsten till Windows Forms-kontroller är inte trådsäker. Multitrådning kan utsätta koden för mycket allvarliga och komplexa buggar. Två eller flera trådar som manipulerar en kontroll kan tvinga kontrollen till ett inkonsekvent tillstånd och leda till konkurrensförhållanden, dödlägen och låsningar. Om du implementerar multitrådning i din app ska du anropa korstrådskontroller på ett trådsäkert sätt. Mer information finns i metodtips för hanterad trådning.
Det finns två sätt att anropa en Windows Forms-kontroll på ett säkert sätt från en tråd som inte skapade den kontrollen. Du kan använda metoden System.Windows.Forms.Control.Invoke för att anropa ett ombud som skapats i huvudtråden, vilket i sin tur anropar kontrollen. Eller så kan du implementera en System.ComponentModel.BackgroundWorker, som använder en händelsedriven modell för att separera arbete som utförts i bakgrundstråden från rapportering av resultaten.
Osäkra korstrådsanrop
Det är osäkert att anropa en kontroll direkt från en tråd som inte skapade den. Följande kodfragment illustrerar ett osäkert anrop till System.Windows.Forms.TextBox kontroll. Händelsehanteraren för Button1_Click
skapar en ny WriteTextUnsafe
tråd som anger huvudtrådens TextBox.Text egenskap direkt.
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
Visual Studio-felsökaren identifierar dessa osäkra trådanrop genom att skapa en InvalidOperationException med meddelandet, korstrådsoperation är ogiltig. En kontroll har nåtts från en annan tråd än den där den skapades.InvalidOperationException inträffar alltid för osäkra korstrådsanrop under Visual Studio-felsökning och kan inträffa vid appkörning. Du bör åtgärda problemet, men du kan inaktivera undantaget genom att ange egenskapen Control.CheckForIllegalCrossThreadCalls till false
.
Säkra korstrådsanrop
Följande kodexempel visar två sätt att på ett säkert sätt anropa en Windows Forms-kontroll från en tråd som inte skapade den:
- Metoden System.Windows.Forms.Control.Invoke, som anropar ett ombud från huvudtråden för att anropa kontrollen.
- En System.ComponentModel.BackgroundWorker komponent, som erbjuder en händelsedriven modell.
I båda exemplen ligger bakgrundstråden i viloläge i en sekund för att simulera arbete som utförs i tråden.
Du kan skapa och köra dessa exempel som .NET Framework-appar från kommandoraden C# eller Visual Basic. Mer information finns i byggande från kommandoraden med csc.exe eller bygga från kommandoraden (Visual Basic).
Från och med .NET Core 3.0 kan du också skapa och köra exemplen som Windows .NET Core-appar från en mapp som har ett .NET Core Windows Forms-<mappnamn>.csproj projektfil.
Exempel: Använd metoden Invoke med ett ombud
I följande exempel visas ett mönster för att säkerställa trådsäkra anrop till en Windows Forms-kontroll. Den frågar egenskapen System.Windows.Forms.Control.InvokeRequired, som jämför kontrollens skapande tråd-ID med det anropande tråd-ID:t. Om tråd-ID:na är desamma anropas kontrollen direkt. Om tråd-ID:erna är olika anropas metoden Control.Invoke med en delegat från huvudtråden, vilket utför det faktiska anropet till kontrollen.
Med SafeCallDelegate
kan du ange TextBox-kontrollens egenskap Text.
WriteTextSafe
-metoden frågar InvokeRequired. Om InvokeRequired returnerar true
, överför WriteTextSafe
SafeCallDelegate
till metoden Invoke för att utföra det faktiska anropet till kontrollen. Om InvokeRequired returnerar false
anger WriteTextSafe
TextBox.Text direkt. Händelsehanteraren Button1_Click
skapar den nya tråden och kör metoden 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
Exempel: Använda en BackgroundWorker-händelsehanterare
Ett enkelt sätt att implementera multitrådning är med komponenten System.ComponentModel.BackgroundWorker, som använder en händelsedriven modell. Bakgrundstråden kör händelsen BackgroundWorker.DoWork, som inte interagerar med huvudtråden. Huvudtråden kör BackgroundWorker.ProgressChanged och BackgroundWorker.RunWorkerCompleted händelsehanterare, som kan anropa huvudtrådens kontroller.
Om du vill göra ett trådsäkert anrop med hjälp av BackgroundWorkerskapar du en metod i bakgrundstråden för att utföra arbetet och binder den till händelsen DoWork. Skapa en annan metod i huvudtråden för att rapportera resultatet av bakgrundsarbetet och binda den till händelsen ProgressChanged eller RunWorkerCompleted. Starta bakgrundstråden genom att anropa BackgroundWorker.RunWorkerAsync.
I exemplet används RunWorkerCompleted händelsehanterare för att ange TextBox-kontrollens egenskap Text. Ett exempel som använder händelsen ProgressChanged finns i 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
Se även
.NET Desktop feedback