Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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 narazić twój kod na bardzo poważne i złożone błędy. Dwa lub więcej wątków manipulujących kontrolką mogą wprowadzić ją w niespójny stan, co może prowadzić do warunków wyścigu, zakleszczeń oraz zawieszeń. W przypadku implementowania wielowątkowości w aplikacji należy wywołać kontrolki międzywątkowe w sposób bezpieczny dla wątków. Aby uzyskać więcej informacji, zobacz Najlepsze praktyki zarządzania wątkami.
Istnieją dwa sposoby bezpiecznego wywoływania kontrolki Windows Forms z wątku, który nie utworzył tej kontrolki. Za pomocą metody System.Windows.Forms.Control.Invoke można wywołać delegata utworzonego w wątku głównym, który z kolei wywołuje kontrolkę. Możesz też zaimplementować System.ComponentModel.BackgroundWorker, która używa modelu opartego na zdarzeniach w celu oddzielenia pracy wykonywanej w wątku w tle od raportowania wyników.
Niebezpieczne wywołania międzywątkowe
Nie można wywołać kontrolki bezpośrednio z wątku, który jej nie utworzył. Poniższy fragment kodu ilustruje niebezpieczne wywołanie kontrolki System.Windows.Forms.TextBox. Program obsługi zdarzeń Button1_Click
tworzy nowy wątek WriteTextUnsafe
, który ustawia bezpośrednio właściwość TextBox.Text wątku głównego.
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 z komunikatem, Nieprawidłowa operacja między wątkami. Kontrolka "" została uzyskana z wątku innego niż ten, na którym została utworzona.InvalidOperationException zawsze występuje w przypadku niebezpiecznych wywołań między wątkami podczas debugowania w programie Visual Studio i może wystąpić w czasie wykonywania aplikacji. Należy rozwiązać ten problem, ale można wyłączyć wyjątek, ustawiając właściwość Control.CheckForIllegalCrossThreadCalls na false
.
Bezpieczne wywołania międzywątowe
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.
- Składnik System.ComponentModel.BackgroundWorker, 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 przy użyciu 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 z nazwą folderu .NET Core Windows Forms <>.csproj pliku projektu.
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 dla właściwości System.Windows.Forms.Control.InvokeRequired, 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 metodę Control.Invoke z delegatem z wątku głównego, co powoduje rzeczywiste wywołanie kontrolki.
SafeCallDelegate
umożliwia ustawienie właściwości Text kontrolki TextBox. Metoda WriteTextSafe
wykonuje zapytania InvokeRequired. Jeśli InvokeRequired zwraca true
, WriteTextSafe
przekazuje SafeCallDelegate
do metody Invoke, aby wykonać rzeczywiste wywołanie kontrolki. Jeśli InvokeRequired zwraca false
, WriteTextSafe
ustawia TextBox.Text bezpośrednio. Program obsługi zdarzeń Button1_Click
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 użycie składnika System.ComponentModel.BackgroundWorker, który korzysta z modelu opartego na zdarzeniach. Wątek w tle uruchamia zdarzenie BackgroundWorker.DoWork, które nie wchodzi w interakcję z głównym wątkiem. Główny wątek uruchamia programy obsługi zdarzeń BackgroundWorker.ProgressChanged i BackgroundWorker.RunWorkerCompleted, które mogą wywoływać kontrolki głównego wątku.
Aby utworzyć bezpieczne wątkowo wywołanie przy użyciu 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 działający w tle, wywołaj BackgroundWorker.RunWorkerAsync.
W przykładzie użyto procedury obsługi zdarzeń RunWorkerCompleted, aby ustawić właściwość Text kontrolki TextBox. Aby zapoznać się z przykładem użycia zdarzenia ProgressChanged, zobacz 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