Operacja wykonywana po kliknięciu przycisku potrafi zamrozić całe okno aplikacji, nawet gdy sama czynność trwa tylko kilka sekund. W tym artykule pokazuję, jak działa BackgroundWorker w C#, jak wykorzystać go w Windows Forms, obsłużyć postęp, anulowanie i błędy oraz kiedy lepiej zastąpić go konstrukcją async/await.
BackgroundWorker ma sens wtedy, gdy interfejs nie może czekać na długą operację
- BackgroundWorker przenosi pracę poza główny wątek interfejsu.
- Obsługuje postęp, anulowanie i wynik za pomocą zdarzeń.
- W metodzie DoWork nie wolno bezpośrednio modyfikować kontrolek.
- Anulowanie jest kooperacyjne, więc kod roboczy musi sam sprawdzać żądanie przerwania.
- W nowych projektach zwykle lepszym wyborem jest Task oraz async/await.

Co robi BackgroundWorker w aplikacji Windows
W aplikacji desktopowej interfejs działa w głównym wątku, często nazywanym wątkiem UI. To on obsługuje kliknięcia, odświeża kontrolki i reaguje na ruch myszy. Jeżeli w tym samym wątku wykonasz duży eksport, skanowanie plików albo wolne zapytanie do urządzenia, okno przestanie odpowiadać.
BackgroundWorker tworzy prosty model pracy w tle. Kod umieszczony w zdarzeniu DoWork wykonuje się poza wątkiem interfejsu, a zdarzenia ProgressChanged i RunWorkerCompleted pozwalają bezpiecznie przekazać informacje z powrotem do okna.
| Element | Rola | Typowy wątek |
|---|---|---|
DoWork |
Wykonuje długą operację | Wątek roboczy |
ProgressChanged |
Aktualizuje pasek postępu i komunikat | Wątek UI |
RunWorkerCompleted |
Obsługuje wynik, błąd lub anulowanie | Wątek UI |
RunWorkerAsync |
Uruchamia pracę w tle | Wywołanie z UI |
CancelAsync |
Prosi o zakończenie pracy | Wywołanie z UI |
Mechanizm jest szczególnie wygodny w starszych aplikacjach Windows Forms, gdzie można dodać komponent z poziomu projektanta Visual Studio i podpiąć zdarzenia. Nie jest jednak magiczną izolacją. Jeżeli kod w tle będzie próbował zmienić właściwość kontrolki, na przykład labelStatus.Text, pojawi się błąd związany z dostępem do kontrolki z niewłaściwego wątku.
Minimalny przykład w Windows Forms
Załóżmy, że aplikacja ma przycisk uruchamiający długą operację, pasek postępu oraz przycisk anulowania. Konfigurację można wykonać w konstruktorze formularza, a samą pracę umieścić w zdarzeniu DoWork.
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
public partial class MainForm : Form
{
private readonly BackgroundWorker worker = new BackgroundWorker
{
WorkerReportsProgress = true,
WorkerSupportsCancellation = true
};
public MainForm()
{
InitializeComponent();
worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
}
private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
return;
buttonStart.Enabled = false;
buttonCancel.Enabled = true;
progressBar1.Value = 0;
labelStatus.Text = "Praca w toku...";
worker.RunWorkerAsync(10);
}
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
var backgroundWorker = (BackgroundWorker)sender;
int numberOfSteps = (int)e.Argument;
for (int step = 1; step <= numberOfSteps; step++)
{
if (backgroundWorker.CancellationPending)
{
e.Cancel = true;
return;
}
Thread.Sleep(500);
int progress = step * 100 / numberOfSteps;
backgroundWorker.ReportProgress(progress, $"Wykonano etap {step} z {numberOfSteps}");
}
e.Result = "Operacja zakończyła się poprawnie.";
}
private void Worker_ProgressChanged(
object sender,
ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
labelStatus.Text = e.UserState?.ToString();
}
private void Worker_RunWorkerCompleted(
object sender,
RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
buttonCancel.Enabled = false;
if (e.Cancelled)
{
labelStatus.Text = "Operacja została anulowana.";
return;
}
if (e.Error != null)
{
labelStatus.Text = $"Błąd: {e.Error.Message}";
return;
}
labelStatus.Text = e.Result?.ToString();
}
private void buttonCancel_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
worker.CancelAsync();
}
}
W przykładzie argument 10 przekazany do RunWorkerAsync trafia do właściwości e.Argument. Wynik przypisany do e.Result odczytujemy dopiero w RunWorkerCompleted. Dzięki temu kod roboczy nie musi znać kontrolek formularza, a warstwa interfejsu zajmuje się tylko prezentacją stanu.
Sprawdzenie IsBusy jest ważne, ponieważ pojedynczy obiekt BackgroundWorker nie powinien być uruchamiany ponownie, gdy poprzednia operacja jeszcze trwa. Bez tego łatwo doprowadzić do podwójnego przetwarzania danych albo konfliktu przy zapisie tego samego pliku.
Postęp i anulowanie wymagają współpracy kodu roboczego
Ustawienie WorkerReportsProgress na true włącza możliwość wywoływania ReportProgress. Pierwszy parametr powinien być liczbą od 0 do 100, a drugi może zawierać dodatkowy komunikat, na przykład nazwę aktualnie przetwarzanego pliku.
Samo wywołanie CancelAsync nie zatrzymuje wykonywanego kodu. Ustawia jedynie flagę CancellationPending. To metoda DoWork musi regularnie ją sprawdzać, ustawić e.Cancel = true i zakończyć działanie.
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
var backgroundWorker = (BackgroundWorker)sender;
foreach (string file in GetFilesToProcess())
{
if (backgroundWorker.CancellationPending)
{
e.Cancel = true;
return;
}
ProcessFile(file);
}
}
W praktyce kontrolę anulowania trzeba umieszczać w miejscach, w których operacja może bezpiecznie się zatrzymać. Jeżeli pojedyncze wywołanie zewnętrznego API trwa 30 sekund, BackgroundWorker nie przerwie go natychmiast. Zatrzyma się dopiero wtedy, gdy wywołanie zwróci sterowanie do kodu, który sprawdza flagę.
Trzeba też pamiętać o możliwym wyścigu. Użytkownik może kliknąć anulowanie dokładnie wtedy, gdy ostatni etap pracy dobiega końca. Dlatego w RunWorkerCompleted zawsze warto obsłużyć osobno trzy sytuacje: poprawny wynik, anulowanie i wyjątek.
Bezpieczne aktualizowanie kontrolek i operacje wejścia wyjścia
Najczęstszy błąd polega na przekazaniu do DoWork referencji do kontrolki i zmianie jej właściwości z wątku roboczego. Taki kod może czasem zadziałać podczas testów, ale pozostaje niebezpieczny i zależny od momentu wykonania.
W Windows Forms do aktualizacji interfejsu służy przede wszystkim ProgressChanged oraz RunWorkerCompleted. W aplikacji WPF podobną rolę może pełnić mechanizm dyspozytora, choć przy nowym kodzie wygodniej zwykle użyć Task, Progress i CancellationToken.
BackgroundWorker dobrze pasuje do zadań takich jak kopiowanie plików, eksport danych, skanowanie katalogów czy komunikacja z urządzeniem. Przy operacjach sieciowych trzeba jednak sprawdzić, czy używana biblioteka nie ma już asynchronicznego API. Blokowanie wątku na synchronicznym żądaniu HTTP nie wykorzystuje możliwości współczesnego .NET tak dobrze jak prawdziwe await.
Podobnie wygląda sytuacja w narzędziach administracyjnych, które wykonują operacje na zdalnych komputerach. Jeżeli aplikacja pokazuje stan połączenia lub uruchamia zadania związane z bezpiecznym dostępem RDP, praca sieciowa nie powinna blokować okna. BackgroundWorker może tu pomóc w starszym projekcie, ale timeouty, ponawianie i możliwość anulowania trzeba zaprojektować osobno.
BackgroundWorker czy async await
W nowych aplikacjach nie zaczynałbym automatycznie od BackgroundWorker. Ten komponent nadal jest użyteczny, szczególnie w istniejącym projekcie Windows Forms i przy prostym modelu zdarzeniowym, ale async/await daje większą elastyczność przy łączeniu wielu operacji, obsłudze wyjątków i pracy z anulowaniem.
| Kryterium | BackgroundWorker | Task i async/await |
|---|---|---|
| Najlepsze zastosowanie | Starsze aplikacje Windows Forms | Nowe aplikacje i złożone przepływy |
| Postęp | ReportProgress |
IProgress |
| Anulowanie | CancelAsync |
CancellationToken |
| Obsługa wielu operacji | Mniej wygodna | Naturalna dzięki await
|
| Integracja z projektantem | Bardzo dobra | Zwykle konfigurowana w kodzie |
Przykład współczesnego podejścia w zdarzeniu przycisku może wyglądać tak:
private CancellationTokenSource? cancellationSource;
private async void buttonStart_Click(object sender, EventArgs e)
{
cancellationSource = new CancellationTokenSource();
buttonStart.Enabled = false;
try
{
var progress = new Progress(value =>
{
progressBar1.Value = value;
});
await ProcessFilesAsync(
progress,
cancellationSource.Token);
labelStatus.Text = "Gotowe";
}
catch (OperationCanceledException)
{
labelStatus.Text = "Anulowano";
}
catch (Exception ex)
{
labelStatus.Text = $"Błąd: {ex.Message}";
}
finally
{
buttonStart.Enabled = true;
cancellationSource.Dispose();
cancellationSource = null;
}
}
private void buttonCancel_Click(object sender, EventArgs e)
{
cancellationSource?.Cancel();
}
Jeśli wykonujesz asynchroniczne operacje wejścia i wyjścia, takie jak odczyt pliku lub zapytanie sieciowe, najlepiej użyć ich wersji z Async. Task.Run ma sens przede wszystkim przy pracy obliczeniowej albo wtedy, gdy musisz odizolować starą, synchroniczną metodę blokującą wątek.
Dobór frameworka i ustawień projektu też może mieć znaczenie przy utrzymywaniu aplikacji desktopowej. Sam BackgroundWorker nie wymaga specjalnej wersji SDK, ale przy problemach z kompilacją lub zgodnością warto sprawdzić wersje Windows SDK używane przez projekt.
Błędy, które najczęściej psują implementację
Uruchamianie wielu instancji tej samej pracy
Przycisk startu powinien być wyłączony na czas działania albo zabezpieczony przez IsBusy. W przeciwnym razie użytkownik może rozpocząć dwa eksporty jednocześnie, a oba wątki będą zapisywać do tego samego miejsca.
Brak obsługi wyjątku
Wyjątek zgłoszony w DoWork trafia do RunWorkerCompletedEventArgs.Error. Nieobsłużenie tej właściwości oznacza zwykle mało czytelny komunikat dla użytkownika albo niekontrolowane zakończenie operacji. Błąd powinien zostać pokazany w interfejsie, zapisany w logu lub przekazany do wyższej warstwy aplikacji.
Założenie, że anulowanie jest natychmiastowe
BackgroundWorker nie może przerwać dowolnej metody w połowie działania. Jeśli kod czeka na sterownik, plik lub połączenie sieciowe, potrzebuje własnych timeoutów i punktów kontrolnych. Anulowanie jest prośbą, a nie wymuszeniem zakończenia wątku.
Raportowanie postępu w złym miejscu
Postęp powinien opisywać realny etap pracy, a nie być sztucznie zwiększany co kilka milisekund. Przy przetwarzaniu plików dobrym rozwiązaniem jest wyliczanie procentu na podstawie liczby elementów, rozmiaru danych albo zakończonych etapów. Pasek postępu, który przez 95 procent czasu stoi na 99 procentach, szybko podważa zaufanie do aplikacji.
Przeczytaj również: winget upgrade --all - jak bezpiecznie aktualizować aplikacje
Używanie komponentu do bardzo krótkich operacji
Jeżeli zadanie trwa kilka milisekund, uruchamianie osobnego mechanizmu w tle może tylko skomplikować kod. BackgroundWorker ma sens wtedy, gdy operacja rzeczywiście blokowałaby interfejs albo mogłaby trwać na tyle długo, że użytkownik zauważy opóźnienie.
Najprostsza reguła wyboru dla aplikacji desktopowej
Jeżeli utrzymujesz starszy projekt Windows Forms, potrzebujesz prostego paska postępu i masz już kod oparty na zdarzeniach, BackgroundWorker nadal jest rozsądnym rozwiązaniem. Jego największą zaletą nie jest nowoczesność, lecz przewidywalny model pracy z interfejsem.
W nowym kodzie wybrałbym zwykle Task, async/await i CancellationToken. Dają lepszą kontrolę nad operacjami sieciowymi, łatwiej łączą się z bibliotekami .NET i pozwalają uniknąć rozbudowanej sieci zdarzeń. Najważniejsze pozostaje jednak to samo: długa praca nie powinna wykonywać się w wątku UI, a każda operacja powinna mieć jasno określone zachowanie przy sukcesie, błędzie i anulowaniu.
