Dela via


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:

  1. Metoden System.Windows.Forms.Control.Invoke, som anropar ett ombud från huvudtråden för att anropa kontrollen.
  2. 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 WriteTextSafeSafeCallDelegate till metoden Invoke för att utföra det faktiska anropet till kontrollen. Om InvokeRequired returnerar falseanger WriteTextSafeTextBox.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