Instrukcje: tworzenie wywołań bezpiecznych wątkowo do kontrolek Windows Forms
Wielowątkowość może zwiększyć wydajność aplikacji Windows Forms, ale dostęp do kontrolek Windows Forms nie jest z natury bezpieczny wątkowo. Wielowątkowość może uwidocznić kod na bardzo poważne i złożone błędy. Co najmniej dwa wątki manipulujące kontrolką mogą wymusić kontrolę na niespójny stan i prowadzić do warunków wyścigu, zakleszczeń i zawiesza się lub zawiesza. W przypadku implementowania wielowątków w aplikacji należy wywołać kontrolki międzywątkowa w bezpieczny wątkowo sposób. Aby uzyskać więcej informacji, zobacz Managed threading best practices (Najlepsze rozwiązania dotyczące zarządzanych wątków).
Istnieją dwa sposoby bezpiecznego wywoływania kontrolki Windows Forms z wątku, który nie utworzył tej kontrolki. Możesz użyć System.Windows.Forms.Control.Invoke metody , aby wywołać delegata utworzonego w głównym wątku, który z kolei wywołuje kontrolkę. Możesz też zaimplementować klasę System.ComponentModel.BackgroundWorker, która używa modelu opartego na zdarzeniach, aby oddzielić pracę wykonaną w wątku w tle od raportowania wyników.
Niebezpieczne wywołania międzywątowe
Nie można wywołać kontrolki bezpośrednio z wątku, który go nie utworzył. Poniższy fragment kodu ilustruje niebezpieczne wywołanie kontrolki System.Windows.Forms.TextBox . Procedura Button1_Click
obsługi zdarzeń tworzy nowy WriteTextUnsafe
wątek, który ustawia właściwość głównego wątku TextBox.Text bezpośrednio.
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
Debuger programu Visual Studio wykrywa te niebezpieczne wywołania wątków, wywołując InvalidOperationException komunikat z nieprawidłową operacją między wątkami. Kontrola "" uzyskiwana z wątku innego niż wątek, w ramach którego została utworzona. Zawsze występuje w InvalidOperationException przypadku niebezpiecznych wywołań między wątkami podczas debugowania programu Visual Studio i może wystąpić w środowisku uruchomieniowym aplikacji. Należy rozwiązać ten problem, ale można wyłączyć wyjątek, ustawiając Control.CheckForIllegalCrossThreadCalls właściwość na false
.
Sejf wywołania między wątkami
W poniższych przykładach kodu pokazano dwa sposoby bezpiecznego wywoływania kontrolki Formularze systemu Windows z wątku, który go nie utworzył:
- Metoda System.Windows.Forms.Control.Invoke , która wywołuje delegata z głównego wątku w celu wywołania kontrolki.
- System.ComponentModel.BackgroundWorker Składnik, który oferuje model oparty na zdarzeniach.
W obu przykładach wątek w tle jest w stanie uśpienia przez jedną sekundę w celu symulowania pracy wykonywanej w tym wątku.
Możesz skompilować i uruchomić te przykłady jako aplikacje .NET Framework z poziomu wiersza polecenia języka C# lub Visual Basic. Aby uzyskać więcej informacji, zobacz Kompilowanie wiersza polecenia za pomocą pliku csc.exe lub Build z wiersza polecenia (Visual Basic).
Począwszy od platformy .NET Core 3.0, można również skompilować i uruchomić przykłady jako aplikacje platformy .NET Core z folderu zawierającego nazwę folderu .NET Core Windows Forms.csproj<.>
Przykład: używanie metody Invoke z pełnomocnikiem
W poniższym przykładzie pokazano wzorzec zapewniający bezpieczne wątkowo wywołania kontrolki Windows Forms. Wykonuje zapytanie względem System.Windows.Forms.Control.InvokeRequired właściwości, która porównuje identyfikator wątku tworzenia kontrolki z identyfikatorem wywołującego wątku. Jeśli identyfikatory wątków są takie same, wywołuje kontrolkę bezpośrednio. Jeśli identyfikatory wątków są inne, wywołuje Control.Invoke metodę z delegatem z wątku głównego, co sprawia, że rzeczywiste wywołanie kontrolki.
Włącza SafeCallDelegate
ustawienie TextBox właściwości kontrolki Text . Metoda WriteTextSafe
wykonuje zapytanie InvokeRequired. Jeśli InvokeRequired funkcja zwraca true
wartość , WriteTextSafe
przekazuje SafeCallDelegate
metodę Invoke do metody , aby wykonać rzeczywiste wywołanie kontrolki. Jeśli InvokeRequired funkcja zwraca false
wartość , WriteTextSafe
ustawia TextBox.Text wartość bezpośrednio. Procedura Button1_Click
obsługi zdarzeń tworzy nowy wątek i uruchamia metodę 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
Przykład: używanie programu obsługi zdarzeń BackgroundWorker
Łatwym sposobem zaimplementowania wielowątku jest składnik, który korzysta z System.ComponentModel.BackgroundWorker modelu opartego na zdarzeniach. Wątek w tle uruchamia BackgroundWorker.DoWork zdarzenie, które nie wchodzi w interakcję z głównym wątkiem. Główny wątek uruchamia BackgroundWorker.ProgressChanged programy obsługi zdarzeń i BackgroundWorker.RunWorkerCompleted , które mogą wywoływać kontrolki głównego wątku.
Aby utworzyć bezpieczne wątkowo wywołanie przy użyciu metody BackgroundWorker, utwórz metodę w wątku w tle, aby wykonać pracę i powiązać ją ze zdarzeniem DoWork . Utwórz inną metodę w wątku głównym, aby zgłosić wyniki pracy w tle i powiązać ją ze zdarzeniem ProgressChanged lub RunWorkerCompleted . Aby uruchomić wątek w tle, wywołaj metodę BackgroundWorker.RunWorkerAsync.
W przykładzie użyto RunWorkerCompleted procedury obsługi zdarzeń, aby ustawić TextBox właściwość kontrolki Text . Przykład użycia zdarzenia można znaleźć w ProgressChanged temacie 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
Zobacz też
.NET Desktop feedback