Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
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)
Metoden Control.InvokeAsync (.NET 9+), som tillhandahåller asynkron hantering av användargränssnittstråden.
Exempel: Använd metoden Control.Invoke:
Metoden Control.Invoke, som anropar ett ombud från huvudtråden för att anropa kontrollen.
Exempel: Använda en BackgroundWorker
En BackgroundWorker komponent, som erbjuder en händelsedriven modell.
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
Tasksom 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
.NET Desktop feedback