Dela via


Hur man hanterar trådövergripande operationer med kontrollen

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 exponera koden för 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. Använd metoden System.Windows.Forms.Control.Invoke för att anropa ett ombud som skapats i huvudtråden, vilket i sin tur anropar kontrollen. Eller 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 button2_Click(object sender, EventArgs e)
{
    WriteTextUnsafe("Writing message #1 (UI THREAD)");
    _ = Task.Run(() => WriteTextUnsafe("Writing message #2 (OTHER THREAD)"));
}

private void WriteTextUnsafe(string text) =>
    textBox1.Text += $"{Environment.NewLine}{text}";
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
    WriteTextUnsafe("Writing message #1 (UI THREAD)")
    Task.Run(Sub() WriteTextUnsafe("Writing message #2 (OTHER THREAD)"))
End Sub

Private Sub WriteTextUnsafe(text As String)
    TextBox1.Text += $"{Environment.NewLine}{text}"
End Sub

Visual Studio-felsökaren identifierar dessa osäkra trådanrop genom att skapa en InvalidOperationException meddelandet korstrådsåtgärden är inte giltig. Åtkomst till kontrollen sker 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 körning av appen. Du bör åtgärda problemet, men du kan inaktivera undantaget genom att ange egenskapen Control.CheckForIllegalCrossThreadCalls till false.

Säkra korstrådsanrop

Windows Forms-program följer ett strikt kontraktsliknande ramverk som liknar alla andra Windows UI-ramverk: alla kontroller måste skapas och nås från samma tråd. Detta är viktigt eftersom Windows kräver att program tillhandahåller en enda dedikerad tråd för att leverera systemmeddelanden till. När Windows Window Manager identifierar en interaktion med ett programfönster, till exempel en tangenttryckning, ett musklick eller ändrar storlek på fönstret, dirigeras informationen till tråden som skapade och hanterar användargränssnittet och omvandlar den till åtgärdsbara händelser. Den här tråden kallas UI-tråden.

Eftersom kod som körs på en annan tråd inte kan komma åt kontroller som skapats och hanteras av användargränssnittstråden, tillhandahåller Windows Forms sätt att arbeta på ett säkert sätt med dessa kontroller från en annan tråd, vilket visas i följande kodexempel:

Exempel: Använd Control.InvokeAsync (.NET 9 och senare)

Från och med .NET 9 innehåller Windows Forms metoden InvokeAsync, vilket ger asynkronvänlig marshaling till användargränssnittstråden. Den här metoden är användbar för asynkrona händelsehanterare och eliminerar många vanliga dödlägesscenarier.

Anmärkning

Control.InvokeAsync är endast tillgängligt i .NET 9 och senare. Det stöds inte i .NET Framework.

Förstå skillnaden: Anropa vs InvokeAsync

Control.Invoke (Skicka – blockera):

  • Skickar synkront ombudet till användargränssnittstrådens meddelandekö.
  • Den anropande tråden väntar tills användargränssnittstråden bearbetar delegeringen.
  • Kan leda till att användargränssnittet fryser när ombudet som har skickats till meddelandekön själv väntar på att ett meddelande ska anlända (dödläge).
  • Användbart när du har resultat som är redo att visas i användargränssnittstråden, till exempel om du inaktiverar en knapp eller anger texten i en kontroll.

Control.InvokeAsync (icke-blockerande):

  • Publicerar asynkront ombudet i användargränssnittstrådens meddelandekö i stället för att vänta på att anropet ska slutföras.
  • Returnerar omedelbart utan att blockera den anropande tråden.
  • Returnerar en Task som kan väntas på slutförande.
  • Perfekt för asynkrona scenarier och förhindrar flaskhalsar i användargränssnittstråden.

Fördelar med InvokeAsync

Control.InvokeAsync har flera fördelar jämfört med den äldre Control.Invoke metoden. Den returnerar en Task som du kan avvakta, vilket gör att den fungerar bra med asynk och await-kod. Det förhindrar också vanliga problem med dödlägen som kan uppstå när asynkron kod blandas med synkrona anrop. Control.Invoke Till skillnad från InvokeAsyncblockerar metoden inte den anropande tråden, vilket håller dina appar dynamiska.

Metoden stöder annullering via CancellationToken, så att du kan avbryta åtgärder när det behövs. Den hanterar också undantag korrekt och skickar tillbaka dem till din kod så att du kan hantera fel på rätt sätt. .NET 9 innehåller kompilatorvarningar (WFO2001) som hjälper dig att använda metoden korrekt.

Omfattande vägledning om asynkrona händelsehanterare och metodtips finns i Översikt över händelser.

Välja rätt InvokeAsync-överbelastning

Control.InvokeAsync tillhandahåller fyra överlagringar för olika scenarier:

Överbelastning Användningsfall Example
InvokeAsync(Action) Synkroniseringsåtgärd, inget returvärde. Uppdatera kontrollegenskaper.
InvokeAsync<T>(Func<T>) Synkroniseringsåtgärd med returvärde. Hämta kontrolltillstånd.
InvokeAsync(Func<CancellationToken, ValueTask>) Async-åtgärd, inget returvärde.* Långvariga uppdateringar av användargränssnittet.
InvokeAsync<T>(Func<CancellationToken, ValueTask<T>>) Asynkron åtgärd med returvärde.* Asynkron datahämtning med resultat.

*Visual Basic har inte stöd för att vänta på en ValueTask.

I följande exempel visas hur du använder InvokeAsync för att på ett säkert sätt uppdatera kontroller från en bakgrundstråd:

private async void button1_Click(object sender, EventArgs e)
{
    button1.Enabled = false;
    
    try
    {
        // Perform background work
        await Task.Run(async () =>
        {
            for (int i = 0; i <= 100; i += 10)
            {
                // Simulate work
                await Task.Delay(100);
                
                // Create local variable to avoid closure issues
                int currentProgress = i;
                
                // Update UI safely from background thread
                await loggingTextBox.InvokeAsync(() =>
                {
                    loggingTextBox.Text = $"Progress: {currentProgress}%";
                });
            }
        });

        loggingTextBox.Text = "Operation completed!";
    }
    finally
    {
        button1.Enabled = true;
    }
}
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles button1.Click
    button1.Enabled = False

    Try
        ' Perform background work
        Await Task.Run(Async Function()
                           For i As Integer = 0 To 100 Step 10
                               ' Simulate work
                               Await Task.Delay(100)

                               ' Create local variable to avoid closure issues
                               Dim currentProgress As Integer = i

                               ' Update UI safely from background thread
                               Await loggingTextBox.InvokeAsync(Sub()
                                                                    loggingTextBox.Text = $"Progress: {currentProgress}%"
                                                                End Sub)
                           Next
                       End Function)

        ' Update UI after completion
        Await loggingTextBox.InvokeAsync(Sub()
                                             loggingTextBox.Text = "Operation completed!"
                                         End Sub)
    Finally
        button1.Enabled = True
    End Try
End Sub

För asynkrona åtgärder som måste köras på användargränssnittstråden använder du asynkron överlagring:

private async void button2_Click(object sender, EventArgs e)
{
    button2.Enabled = false;
    try
    {
        loggingTextBox.Text = "Starting operation...";

        // Dispatch and run on a new thread, but wait for tasks to finish
        // Exceptions are rethrown here, because await is used
        await Task.WhenAll(Task.Run(SomeApiCallAsync),
                           Task.Run(SomeApiCallAsync),
                           Task.Run(SomeApiCallAsync));

        // Dispatch and run on a new thread, but don't wait for task to finish
        // Exceptions are not rethrown here, because await is not used
        _ = Task.Run(SomeApiCallAsync);
    }
    catch (OperationCanceledException)
    {
        loggingTextBox.Text += "Operation canceled.";
    }
    catch (Exception ex)
    {
        loggingTextBox.Text += $"Error: {ex.Message}";
    }
    finally
    {
        button2.Enabled = true;
    }
}

private async Task SomeApiCallAsync()
{
    using var client = new HttpClient();
    
    // Simulate random network delay
    await Task.Delay(Random.Shared.Next(500, 2500));

    // Do I/O asynchronously
    string result = await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md");

    // Marshal back to UI thread
    await this.InvokeAsync(async (cancelToken) =>
    {
        loggingTextBox.Text += $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}";
    });

    // Do more async I/O ...
}
Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles button2.Click
    button2.Enabled = False
    Try
        loggingTextBox.Text = "Starting operation..."

        ' Dispatch and run on a new thread, but wait for tasks to finish
        ' Exceptions are rethrown here, because await is used
        Await Task.WhenAll(Task.Run(AddressOf SomeApiCallAsync),
                           Task.Run(AddressOf SomeApiCallAsync),
                           Task.Run(AddressOf SomeApiCallAsync))

        ' Dispatch and run on a new thread, but don't wait for task to finish
        ' Exceptions are not rethrown here, because await is not used
        Call Task.Run(AddressOf SomeApiCallAsync)

    Catch ex As OperationCanceledException
        loggingTextBox.Text += "Operation canceled."
    Catch ex As Exception
        loggingTextBox.Text += $"Error: {ex.Message}"
    Finally
        button2.Enabled = True
    End Try
End Sub

Private Async Function SomeApiCallAsync() As Task
    Using client As New HttpClient()

        ' Simulate random network delay
        Await Task.Delay(Random.Shared.Next(500, 2500))

        ' Do I/O asynchronously
        Dim result As String = Await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md")

        ' Marshal back to UI thread
        ' Extra work here in VB to handle ValueTask conversion
        Await Me.InvokeAsync(DirectCast(
                Async Function(cancelToken As CancellationToken) As Task
                    loggingTextBox.Text &= $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}"
                End Function,
            Func(Of CancellationToken, Task)).AsValueTask() 'Extension method to convert Task
        )

        ' Do more Async I/O ...
    End Using
End Function

Anmärkning

Om du använder Visual Basic använde det tidigare kodfragmentet en tilläggsmetod för att konvertera en ValueTask till en Task. Tilläggsmetodkoden är tillgänglig på GitHub.

Exempel: Använd metoden Control.Invoke

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 efter egenskapen System.Windows.Forms.Control.InvokeRequired, som jämför kontrollens skapande tråd-ID med det anropande tråd-ID. Om de är annorlunda bör du anropa metoden Control.Invoke.

Med WriteTextSafe kan du ange TextBox-kontrollens egenskap Text till ett nytt värde. Metoden frågar InvokeRequired. Om InvokeRequired returnerar trueanropar WriteTextSafe rekursivt sig själv och skickar metoden som ombud till metoden Invoke. Om InvokeRequired returnerar false, så sätter WriteTextSafeTextBox.Text direkt. Händelsehanteraren Button1_Click skapar den nya tråden och kör metoden WriteTextSafe.

private void button1_Click(object sender, EventArgs e)
{
    WriteTextSafe("Writing message #1");
    _ = Task.Run(() => WriteTextSafe("Writing message #2"));
}

public void WriteTextSafe(string text)
{
    if (textBox1.InvokeRequired)
        textBox1.Invoke(() => WriteTextSafe($"{text} (NON-UI THREAD)"));

    else
        textBox1.Text += $"{Environment.NewLine}{text}";
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    WriteTextSafe("Writing message #1")
    Task.Run(Sub() WriteTextSafe("Writing message #2"))

End Sub

Private Sub WriteTextSafe(text As String)

    If (TextBox1.InvokeRequired) Then

        TextBox1.Invoke(Sub()
                            WriteTextSafe($"{text} (NON-UI THREAD)")
                        End Sub)

    Else
        TextBox1.Text += $"{Environment.NewLine}{text}"
    End If

End Sub

Mer information om hur Invoke skiljer sig från InvokeAsyncfinns i Förstå skillnaden: Anropa vs InvokeAsync.

Exempel: Använda en BackgroundWorker

Ett enkelt sätt att implementera scenarier med flera trådar samtidigt som du garanterar att åtkomsten till en kontroll eller ett formulär endast utförs på huvudtråden (UI-tråden) är med komponenten System.ComponentModel.BackgroundWorker , som använder en händelsedriven modell. Bakgrundstråden utlöser 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.

Viktigt!

Komponenten BackgroundWorker är inte längre den rekommenderade metoden för asynkrona scenarier i Windows Forms-program. Även om vi fortsätter att stödja den här komponenten för bakåtkompatibilitet, adresserar den bara avlastning av processorarbetsbelastning från användargränssnittstråden till en annan tråd. Den hanterar inte andra asynkrona scenarier som fil-I/O eller nätverksåtgärder där processorn kanske inte arbetar aktivt.

För modern asynkron programmering använder du async metoder med await i stället. Om du uttryckligen behöver avlasta processorintensivt arbete kan du använda Task.Run för att skapa och starta en ny uppgift, som du sedan kan vänta på som alla andra asynkrona åtgärder. Mer information finns i Exempel: Använd Control.InvokeAsync (.NET 9 och senare) och åtgärder och händelser mellan trådar.

Om du vill göra ett trådsäkert anrop med hjälp av BackgroundWorkerhanterar du händelsen DoWork. Det finns två händelser som bakgrundsarbetaren använder för att rapportera status: ProgressChanged och RunWorkerCompleted. Händelsen ProgressChanged används för att kommunicera statusuppdateringar till huvudtråden, och händelsen RunWorkerCompleted används för att signalera att bakgrundsarbetaren har slutförts. Starta bakgrundstråden genom att anropa BackgroundWorker.RunWorkerAsync.

Exemplet räknar från 0 till 10 i händelsen DoWork och pausar en sekund mellan varje siffra. Den använder ProgressChanged händelsehanterare för att rapportera numret tillbaka till huvudtråden och ange TextBox-kontrollens egenskap Text. För att den ProgressChanged händelsen ska fungera måste egenskapen BackgroundWorker.WorkerReportsProgress anges till true.

private void button1_Click(object sender, EventArgs e)
{
    if (!backgroundWorker1.IsBusy)
        backgroundWorker1.RunWorkerAsync(); // Not awaitable
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    int counter = 0;
    int max = 10;

    while (counter <= max)
    {
        backgroundWorker1.ReportProgress(0, counter.ToString());
        System.Threading.Thread.Sleep(1000);
        counter++;
    }
}

private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) =>
    textBox1.Text = (string)e.UserState;
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    If (Not BackgroundWorker1.IsBusy) Then
        BackgroundWorker1.RunWorkerAsync() ' Not awaitable
    End If

End Sub

Private Sub BackgroundWorker1_DoWork(sender As Object, e As ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork

    Dim counter = 0
    Dim max = 10

    While counter <= max

        BackgroundWorker1.ReportProgress(0, counter.ToString())
        System.Threading.Thread.Sleep(1000)

        counter += 1

    End While

End Sub

Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
    TextBox1.Text = e.UserState
End Sub