Czekanie na zakończenie BackgroundWorker'a bez zamrażania GUI

0

Zrobiłem małą aplikację, w której w BackgroundWorkerze zmieniam pewne elementy GUI. I to działa bez zarzutów. Natomiast mam problem z pewnym rozszerzeniem tego programu. Chodzi o to, że kiedy wywołuje się metodę BackgroundWorker.RunWorkerAsync() to program idzie dalej. W sensie, że wykonują się kolejne linijki w z kodu programu. A jak zrobić, aby po wywołaniu tej metody, program wisiał i czekał na zakończenie pracy worker'a, ale jednocześnie nie powodowało to zamrożenia GUI? Coś jak Join z klasy Thread, ale żeby GUI zmieniło się zgodnie z tym co worker robi.
Próbowałem odpalać workera w wątku, gdzie czekałem na jako zakończenie przez Join, ale GUI się mroziło. Próbowałem używać klasy EventWaitHandle i metody WaitOne żeby czekać, ale to działało tak samo jak wątek z Join. Także brakuje mi pomysłów. Próbowałem w oddzielnym wątku sprawdzać status pracy worker'a, ale bez Join i tak program szedł dalej. Z Join było w porządku ale GUI było zamrożone.
Jakieś pomysły na rozwiązanie? Jeżeli to ma jakieś znaczenie, to korzystam z WPF'a i obiektu Dispatcher do modyfikacji GUI (ale tak jak mówię, jeśli nie chcę czekać na zakończenie zmian, to BackgroundWorker działa bez zarzutów. Mi chodzi o to, żeby poczekać na zakończenie jego pracy, aby program wykonywał kolejne swoje linijki.

0
Lena(R) napisał(a):

Mi chodzi o to, żeby poczekać na zakończenie jego pracy, aby program wykonywał kolejne swoje linijki.

Myślę, że nie trzeba kombinować i wystarczy przenieść ten kod do zdarzenia BackgroundWorker.RunWorkerCompleted.

0

Chyba nie do końca się rozumiemy (albo nie widzę co fizycznie przenieść do RunWorkerCompleted). Bo chodzi o to, że ja wywołuje worker'a, a potem w zależności od tego w jakim stanie skończył pracę (czy był jakiś wyjątek, czy anulowano, czy poszło bez problemów) inaczej reagować. Mam klasę dziedziczącą po BackgroundWorker i w niej tak przeciążyłem OnRunWorkerCompleted.

protected override void OnRunWorkerCompleted(RunWorkerCompletedEventArgs e)
{
    if (e.Error != null)
    {
        State = WorkerState.Error;
    }
    else if (e.Cancelled)
    {
        State = WorkerState.Cancelled;
    }
    else
    {
        State = WorkerState.Finished;
    }
}

I w zależności od tego, co przypisane do State, chciałbym aby program inaczej kontynuował swoją pracę. Dlatego zależy mi na tym, żeby zrobić coś na zasadzie:

worker.RunWorkerAsync();

//
// coś tutaj zrobić żeby zaczekać na zakończenie workera bez zamrożenia GUI
//

// a potem
if (worker.State == WorkerState.Error)
{
      // to coś tam
}
else if (worker.State  == WorkerState.Cancelled)
{
     // to coś tam
}
else if (worker.State == WorkerState.Finished)
{
     // to coś tam
}
0

Przecież worker sam z siebie działa nie mrożąc GUI, prawda?
To, co chcesz robić po zakończeniu działania BackgroundWorkera, musisz umieść w RunWorkerCompleted. Możesz oczywiście z tej metody wywołać inną, która zrobi to, co chcesz, albo nawet rzucić swoje własne zdarzenie, którego metoda obsługująca wykona to, o co Ci chodzi. Ale tak czy siak, operacje wykonywane po zakończeniu pracy przez worker, muszą się zacząć w RunWorkerCompleted.

0

No tak, wszystko jasne. GUI się nie zamraża. Ale przecież odpalenie RunWorkerAsync() powoduje, że program idzie dalej (jak Thread.Start()). I nawet gdybym przeniósł te if-y do RunWorkerCompleted, to to nie spowoduje zatrzymania programu.
Nie wiem jak to dokładniej wytłumaczyć, ale zależy mi na tym, żeby w jakiś sposób zaczekać na zakończenie działania BackgroundWorker'a przed ruszeniem dalej (tak jak w komentarzu). Nie ważne jest co mam dalej, chciałbym zaczekać na wynik pracy. Bo przecież nawet gdybym dodał eventa to i tak nie spowoduje to, że program będzie czekał na zakończenie pracy, tylko że kiedy złapię zdarzenie to coś zrobi. A mi zależy na tym, żeby to coś było w konkretnym miejscu zrobione. Tak jakby miał czekać w pętli aż worker skończy swoją pracę (tylko że zwykła pętla powoduje zamrożenie GUI).

0

Pomyśl w swoim programie w sposób taki : gdy coś się stanie zrobię coś a nie wykonuję metody od A do Z i przy B czekam . Jeśli jednak dalej chcesz pisać liniowo a nie zdarzeniowo, to skorzystaj z Task ( Backgroundworker to Task ale opakowany w synchronizację ) lub z najnowszego trendy "await" ( http://www.google.pl/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0CDEQFjAA&url=http%3A%2F%2Fwww.pzielinski.com%2F%3Fp%3D192&ei=UlvdUdT4EoX3sga7gYHIBg&usg=AFQjCNFyH9n72lIZwHBy9xMNF91UYIIm3A&sig2=7tPLw0nqTa8ovgqidW3z4A&bvm=bv.48705608,d.Yms )

0
Lena(R) napisał(a):

No tak, wszystko jasne. GUI się nie zamraża. Ale przecież odpalenie RunWorkerAsync() powoduje, że program idzie dalej (jak Thread.Start()). I nawet gdybym przeniósł te if-y do RunWorkerCompleted, to to nie spowoduje zatrzymania programu.

Co to znaczy "program idzie dalej"?
Mi chodzi o to, żeby całe to "dalej" było przeniesione do RunWorkerCompleted. Wtedy się wykona dopiero, gdy worker skończy pracę, a o to właśnie Ci chodzi.

0

Ech...może spróbuje inaczej to wytłumaczyć na przykładzie. Robię mniej więcej coś takiego:

class Worker : System.ComponentModel.BackgroundWorker
{

    public Worker()
    {
        this.WorkerReportsProgress = true;
    }

    protected override void OnDoWork(System.ComponentModel.DoWorkEventArgs e)
    {
        base.OnDoWork(e);
        const int length = 100000;
        for (int i = 0; i <= length; i++)
        {
            int p = (int) Math.Min(100.0, ((double) i / (double) length) * 100.0);
            ReportProgress(p, e.Argument);
        }
    }

    protected override void OnProgressChanged(System.ComponentModel.ProgressChangedEventArgs e)
    {
        base.OnProgressChanged(e);
        Label l = e.UserState as Label;
        l.Dispatcher.BeginInvoke(new Action<int>((c) => { l.Content = c; }), e.ProgressPercentage);
    }
}

// A potem w jakiejś metodzie:
Worker w = new Worker();
w.RunWorkerAsync(label);    // <- gdzie 'label' to instancja etykiety stworzona przez designer GUI

// A potem chciałbym wykonywać inne operacje, ale dopiero po zakończeniu działania workera
// czyli zrobić coś na zasadzie Thread.Join(), żeby poczekać aż worker zakończy pracę
// ale żeby zapobiec zamrożenia GUI, bo do tej pory wszelkie próby zamrażają GUI.

Chodzi o to, że działanie workera jest rozpoczynane albo przy ładowaniu okienka, albo reaguje na naciśnięcie buttona i służy m.in. jako taki pasek postępu przy wczytywaniu danych z pliku, czy przeszukiwanie plików. Więc potrzebuje poczekać na zakończenie tych wszystkich operacji, żeby kontynuacja programu niczego nie popsuła i bazowała na poprawnych danych. A jednocześnie chciałbym wiedzieć jaki jest stan postępu tych operacji.

Może wykorzystanie mutexów, albo semaforów mogłoby tutaj jakoś pomóc?

1

jeszcze raz:

PRZENIEŚ WSZYSTKO CO CHCESZ ZROBIĆ POTEM DO RUNWORKERCOMPLETED

jeśli nie chcesz żeby user w tym czasie mógł cokolwiek naciskać to zablokuj formę poprzez

Form1.Enabled = false;

i odblokuj też w RUNWORKERCOMPLETED.

czyli kod będzie wyglądał w stylu:

var w = new BackgroundWorker { WorkerReportsProgress = true };

this.Enabled = false; // zablokowanie okna

w.DoWork += (_sender, args) =>
{
    // TUTAJ ROBISZ TO CO CHCESZ W WORKERZE
    for (var i = 0; i <= 100; i++)
    {
        w.ReportProgress(i);
        Thread.Sleep(30);
    }
};

w.ProgressChanged += (Object _sender, System.ComponentModel.ProgressChangedEventArgs args) =>
{
    progressBar1.Value = args.ProgressPercentage;
};

w.RunWorkerCompleted += (_sender, args) =>
{
    // TUTAJ ROBISZ RZECZY KTÓRE CHCESZ ROBIĆ POTEM
    this.Enabled = true;
    progressBar1.Value = 0;
    MessageBox.Show("Gotowe");
};

w.RunWorkerAsync();

Jeśli to z jakiegoś powodu ci nie pasuje to jedynie możesz utworzyć kolejny wątek który faktycznie będzie czekał na workera. Jeżeli chcesz żeby proces GUI czekał na jakiś wątek to logiczne że GUI będzie zablokowane.

Tego niżej pod żadnym pozorem nie czytaj!

możesz też w pętli wywoływać "Application.DoEvents()" dzięki któremu program obsłuży kolejkę komunikatów, w tym między innymi przerysowanie kontrolek i "Thread.Sleep" czekając aż worker skończy pracę.
TYLKO ŻE W TAKIM PRZYPADKU W OGÓLE NIE POTRZEBUJESZ BACKGROUNDWORKERA BO JEDNOCZEŚNIE DZIAŁAĆ BĘDZIE TYLKO JEDEN WĄTEK I RÓWNIE DOBRZE MÓGŁBYŚ TO WSZYSTKO ROBIĆ W JEDNYM</span>

0

Ok, to ja rozumiem.
Natomiast klasa workera jest niewidoczna dla GUI, bo jest opakowana jeszcze w inny obiekt. Także nie mogę tam dopisać żadnych Enabled = ..., bo wtedy w każdym zachowaniu tego workera musiałbym to dopisywać, a nie zawsze to ma zależeć od GUI.

Jeśli to z jakiegoś powodu ci nie pasuje to jedynie możesz utworzyć kolejny wątek który faktycznie będzie czekał na workera. Jeżeli chcesz żeby proces GUI czekał na jakiś wątek to logiczne że GUI będzie zablokowane.

Po tej wypowiedzi, chyba doszedłem czemu się nie rozumiemy. GUI o którym ja ciągle pisze, to dodatkowe okienko które wyskakuje jak progressbar i się animuje jak pasek i zmieniany tekst. Po zakończeniu pracy znika i kontynuowane są inne zadania. Więc po wywołaniu workera, pojawia się progress. I mi zależy na tym, żeby zaczekać na zakończenie tego progressa i dopiero kontynuować pracę. To że GUI pod spodem, rodzica, czy jak to zwał, ogólnie całe inne, które jest aktualnie wyświetlone przez czas działania workera jest zamrożone i to rozumiem.
Natomiast dlaczego kiedy opakowuje workera w wątek:

class WorkerWaitHandle : IDisposable
{
private readonly BackgroundWorker worker;
private Thread thread;
private readonly object syncObject = new object();

private bool completed = false;

public bool Completed
{
    get 
    {
        lock (syncObject)
        {
            return completed;
        }
    }
    set
    {
        lock (syncObject)
        {
            completed = value;
        }
    }
}


public WorkerWaitHandle(BackgroundWorker worker)
{
    if (worker == null)
    {
        throw new ArgumentNullException("worker");
    }
    this.worker = worker;
    this.worker.RunWorkerCompleted += worker_RunWorkerCompleted;
}

void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    Completed = true;
}

public void Wait()
{
    Completed = false;
    thread = new Thread((o) =>
        {
            BackgroundWorker w = o as BackgroundWorker;
            w.RunWorkerAsync();    // to też próbowałem odpalać poza wątkiem i w ogóle stało i czekało nie wiem na co
            while (!Completed)
            {
                Thread.Sleep(200);
            }
        });
    thread.Start(worker);
    thread.Join();
}

#region Disposable Pattern Implementation

public bool Disposed { get; private set; }

~WorkerWaitHandle()
{
    Dispose(false);
}

public void Dispose()
{
    Dispose(true);
    GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
    if (!Disposed)
    {
        if (thread != null && thread.IsAlive && !thread.Join(2000))
        {
            thread.Abort();
        }
        if (disposing && worker != null)
        {
            worker.Dispose();
        }
        Disposed = true;
    }
}

#endregion

}

to GUI które on otwiera z tym progressbarem jest zamrożone, albo w ogóle nie zdąży się pojawić, albo tylko mignie: pojawi się i zniknie? Nie rozumiem tego, przecież odpalam wątek i czekam na jego zakończenie, a w tle (jak dla mnie) wykonują się inne operacje. A tutaj tak jakby oczekiwanie na inny wątek coś blokowało. Może ja czegoś nie wiem o workerze albo o sposobie działania Thread? I skoro worker odpala inny wątek, to czemu ten inny wątek jest (tak to wygląda) zależny od wątku, w którym został wywołany? To mi wygląda jakby Thread.Sleep() blokował wszystkie wątki, a w dokumentacji jest napisane, że blokuje obecny wątek.
Może muszę zastosować jakieś bardziej zaawansowane techniki obsługi takiej sytuacji.

1

Coś ty w tym kodzie narobił? Wywal to wszystko i zrób to od początku tak jak ci zostało już 10 razy w tym wątku napisane.

0

Obszedłem problem zupełnie inaczej. Po prostu zamiast w workerze robić Show() na oknie który on do tej pory otwierał w DoWork na początku pracy, wywołuje ręcznie ShowDialog() poza workerem i działa bez zarzutów. To idealne rozwiązanie tego mojego problemu, więcej mi nie potrzeba.

1

W WPFie- Show() otwiera normalne okno (konstruktor może przyjąć parametr Owner określający właściciela okna ), ShowDialog() otwiera okno dialogowe i blokuje operacje GUI na pozostałych oknach programu do czasu zamknięcia dialogu ( jak np. OpenFileDialog).

Ja ostatnio zaczytuję się z async,await i powiem ci , że w tej kwestii MS dał sobie dużego plusa. Pozbywasz się marshalowania (Dispathcer w wpf), testy są prostsze a samo łapanie wyjątków banalne. I co ważne używasz blisko 99% mocy procesora bo nie tracisz czasu na synchronizację wątków.

0

W WPFie- Show() otwiera normalne okno (konstruktor może przyjąć parametr Owner określający właściciela okna ), ShowDialog() otwiera okno dialogowe i blokuje operacje GUI na pozostałych oknach programu do czasu zamknięcia dialogu ( jak np. OpenFileDialog).

No mi właśnie zależało na tym, żeby zablokować inne okna prócz tego które otwiera worker. I ShowDialog() radzi sobie z tym doskonale. Zwykły Show() otwierał okno ale też "udostępniał" inne i moje próby blokowania kończyły się również zamrażaniem GUI które odpalał worker. Teraz działa i bardziej udoskonalać tej operacji nie potrzebuje.

Ja ostatnio zaczytuję się z async,await i powiem ci , że w tej kwestii MS dał sobie dużego plusa. Pozbywasz się marshalowania (Dispathcer w wpf), testy są prostsze a samo łapanie wyjątków banalne. I co ważne używasz blisko 99% mocy procesora bo nie tracisz czasu na synchronizację wątków.

No ja czytałem tylko o tym i bardzo fajnie to działa na papierze. Aż ciężko mi uwierzyć żeby to było takie fajne i proste w użyciu. Na własnej skórze nie miałem jeszcze okazji tego testować. W tym wypadku trochę za dużo już miałem kodu napisanego i mówiąc szczerze trochę się bałem przerabiać na async-await, bo nie wiedziałem co z tego wyjdzie (tym bardziej, że byłby to mój pierwszy kod tego typu). Może następnym razem.
Generalnie, lubię technologie MS (.NET, MSSQL, Windows, Office, poczta, ...) i jestem pewnie, że to tylko kwestia czasu kiedy zacznę i to wykorzystywać. Obecnie jestem na etapie zgłębiania się w WPF, bo widzę że to ma ogromne możliwości, których nie widać na pierwszy rzut oka. Następny etap to usługi, czyli pewnie WCF i sam ASP.NET do robienia stron.

1 użytkowników online, w tym zalogowanych: 0, gości: 1