BackgroundWorker w C# czy async/await? Co wybrać?

Bruno Krawczyk • 11 września 2026
Diagram przedstawia przepływ zadań w C# z użyciem `BackgroundWorker` (implikowanego przez `Task.Delay` i `ContinueOnCurrentContext`). Pokazuje synchronizację wątków i kontynuacje zadań.

Spis treści

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.

Ilustracja porównuje Async/Await z Task/Thread w C#. Pokazuje kelnera obsługującego gości, zadania w tle i blokady, jak w przypadku backgroundworker.

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.

FAQ - Najczęstsze pytania

Kod w zdarzeniu DoWork działa na wątku roboczym i nie powinien bezpośrednio zmieniać kontrolek. Postęp oraz komunikaty należy przekazywać przez ProgressChanged, a wynik, błąd lub anulowanie obsługiwać w RunWorkerCompleted, wykonywanym na wątku UI.

CancelAsync jedynie ustawia flagę CancellationPending. Kod w DoWork musi regularnie ją sprawdzać, ustawić e.Cancel = true i zakończyć działanie. Jeśli pojedyncze wywołanie pliku, sterownika lub API trwa długo, anulowanie nastąpi dopiero po odzyskaniu sterowania przez kod roboczy.

BackgroundWorker pasuje głównie do starszych aplikacji Windows Forms z prostym modelem zdarzeniowym, paskiem postępu i istniejącą obsługą komponentu. W nowych projektach zwykle lepszym wyborem są Task, async/await i CancellationToken, szczególnie przy łączeniu wielu operacji oraz asynchronicznym wejściu i wyjściu.

Ustaw WorkerReportsProgress na true i wywołuj ReportProgress z wartością od 0 do 100 oraz opcjonalnym komunikatem. Wynik przypisz do e.Result, a następnie odczytaj go w RunWorkerCompleted. W tym samym zdarzeniu sprawdź osobno e.Cancelled i e.Error.

Oceń artykuł

Ocena: 4.73 Liczba głosów: 11

Tagi

backgroundworker
async/await
windows forms
wielowątkowość
anulowanie
Autor Bruno Krawczyk
Bruno Krawczyk
Mam na imię Bruno i od 8 lat zgłębiam tajniki programowania w ekosystemie .NET, chmury Azure oraz sztucznej inteligencji. Moja przygoda z technologią zaczęła się od ciekawości, jak złożone systemy mogą ułatwiać codzienne życie i rozwiązywać realne problemy. Dziś moją misją jest dzielenie się tą wiedzą, starając się przybliżyć nawet najbardziej skomplikowane zagadnienia w sposób zrozumiały i przystępny dla każdego. W moich artykułach na kursdotnet.pl skupiam się na praktycznych aspektach, analizuję najnowsze trendy i weryfikuję informacje, aby dostarczyć Wam treści, które są nie tylko dokładne i aktualne, ale przede wszystkim użyteczne w Waszej własnej ścieżce rozwoju technologicznego.

Udostępnij artykuł

Napisz komentarz