sobota, 29 grudnia 2012

70-511 Rozdział 9 - programowanie asynchroniczne

Programowanie asynchroniczne ma na celu polepszenie interakcji aplikacji z użytkownikiem. Przenosząc długotrwałe operacje na inny wątek, zapewniamy, że interfejs naszej aplikacji nie zostanie zamrożony.
Komponentem, który umożliwia proste przeniesienie długotrwałych operacji na inny wątek, znany także z Windows Forms jest BackgroundWorker.
W momencie, gdy na obiekcie klasy BackgroundWorker wywołamy metodę RunWorkerAsync zostaje wywołane zdarzenie DoWork. Kod podpięty pod to zdarzenie zostanie wykonany na osobnym wątku. Niektóre z właściwości obiektu BackgroundWorker:
  • CancellationPending - flaga czy anulowano zadanie
  • IsBusy - flaga czy BW wykonuje zadanie
  • WorkerReportsProgress - flaga czy BW może powiadamiać o postępie pracy
  • WorkerSupportsCancellation - flaga czy operacja wykonywana przez BW może zostać anulowana
  • CancelAsync - przerywa aktualnie wykonywaną operację
  • ReportProgress - wywołuje zdarzenie ProgressChanged
  • RunWorkerAsync - uruchamia operacje
  • DoWork - zdarzenie, które jest wywoływane w momencie wywołania metody RunWorkerAsync 
  • ProgressChanged - zdarzenie wywoływane w momencie wywołania metody ReportProgress
  • RunWorkerCompleted - zdarzenie wywoływane w momencie zakończenia operacji przez BW
Podczas pracy z BW możemy mieć następujące zadania do zrealizowania:
  • przesłanie danych do naszego nowego wątku - parametr przekazujemy w wywołaniu metody RunWorkerAsync, a odebranie parametru następuje przez właściwość DoWorkEventArgs.Argument
  • powiadomienie, że proces zakończył się - realizowane poprzez obsłużenie zdarzenia RunWorkerCompleted
  • zwrócenie obliczanej wartości - przypisujemy wartość do właściwości - DoWorkEventArgs.Result, a odbieramy ją w zdarzeniu RunWorkerCompleted.Result
  • anulowanie operacji - ustawiamy flagę WorkerSupportsCancellation; w zdarzeniu DoWork implementujemy logikę, która sprawdza co jakiś czas flagę CancellationPending i jeżeli flaga jest ustawiona, przerywa operację
  • raportowanie postępów - ustawiamy flagę WorkerReportsProgress; w kodzie, metody która wykonuje długotrwałe obliczenia wywołujemy metodę ReportProgress do której przekazujemy informację o procencie wykonanej pracy
Przykładowa realizacja opisanych powyżej punktów:

Code:
public partial class MainWindow : Window
    {
        private BackgroundWorker backgroundWorker;

        public MainWindow()
        {
            InitializeComponent();

            backgroundWorker = new BackgroundWorker {WorkerReportsProgress = true, WorkerSupportsCancellation = true};
            backgroundWorker.DoWork += (o, args) =>
            {
                var number = double.Parse(args.Argument.ToString());
                double tmp = 0;
                for (int i = 1; i < 101; i++)
                {
                    if (backgroundWorker.CancellationPending)
                    {
                        args.Cancel = true;
                        return;
                    }
                    tmp += Math.Sqrt(number*i);
                    Thread.Sleep(50);
                    backgroundWorker.ReportProgress(i);
                }

                args.Result = tmp;
            };

            backgroundWorker.RunWorkerCompleted += (sender, args) =>
                                                       {
                                                           if (!args.Cancelled)
                                                           {
                                                               tbResult.Text = "Zadanie ukończone. Wynik: " +
                                                                               args.Result;
                                                           }
                                                           else
                                                           {
                                                               tbResult.Text = "Zadanie zostało przerwane";
                                                           }
                                                       };

            backgroundWorker.ProgressChanged += (sender, args) =>
                                                    {
                                                        pbProgress.Value = args.ProgressPercentage;
                                                    };
        }

        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            backgroundWorker.RunWorkerAsync(tbNumber.Text);
        }

        private void btnStop_Click(object sender, RoutedEventArgs e)
        {
            backgroundWorker.CancelAsync();
        }
    }






Aplikacja do pobrania z linka: http://sdrv.ms/V0bXkg



Delegaty
Delegaty umożliwiają implementację asynchronicznych metod. Każda delegata posiada metody: BeginInvoke oraz EndInvoke. Wywołanie pierwszej z nich powoduje stworzenie osobnego wątku i wykonania w nim wskazywanej metody. Druga metoda pozwala na pobranie rezultatu wykonania metody.
Delegaty możemy użyć na trzy sposoby w celu tworzenia metod asynchronicznych:

  • wywołujemy BeginInvoke, wykonujemy zadanie a następnie wywołujemy EndInvoke na tym samym wątku
  • wywołujemy BeginInvoke a następnie oczekując na zakończenie wywołania, wykonujemy inne operacje
  • wywołanie metody po zakończeniu pracy wątku w tle
Pierwsze rozwiązanie jest najprostsze w wykonaniu, jednak nie jest idealne. W momencie wywołania metody EndInvoke nastąpi zablokowanie wątku głównego i oczekiwanie aż wątek w tle zakończy swoją pracę:

Code:
            MyDelegate myDelegate = DoSomething;
            myDelegate(null);
            IAsyncResult result = myDelegate.BeginInvoke(5, null, null);
            lblOnTheSameThreadResult.Text = myDelegate.EndInvoke(result).ToString();

Drugi przypadek zakład iż, w czasie oczekiwania na rezultat wykonania się operacji w innym wątku, będziemy wykonywać inne rzeczy w głównym wątku:

Code:
            MyDelegate myDelegate = DoSomething;
            myDelegate(null);
            IAsyncResult result = myDelegate.BeginInvoke(5, null, null);
            while (!result.IsCompleted)
            {
                //Do some operations here
            }
            lblPoolingResult.Text = myDelegate.EndInvoke(result).ToString();

Ostatnia możliwość zakłada, iż rezultat wykonania operacji asynchronicznej nie będzie nam potrzebny w wątku, który go wykonał. Po zakończeniu obliczeń, zostanie wywołana metoda, w której możemy odebrać naszą wartość:

Code:
            MyDelegate myDelegate = DoSomething;
            myDelegate(null);
            IAsyncResult result = myDelegate.BeginInvoke(5,
                            new AsyncCallback(ar =>
                                                {
                                                    var d = (MyDelegate) ar.AsyncState;
                                                    double res = d.EndInvoke(ar);
                                                    Dispatcher.Invoke(() =>
                                                                          {
                                                                              lblCallbackResult.Text = res.ToString();
                                                                          });
                                                }), myDelegate);

Kod do pobrania: http://sdrv.ms/RPK8iZ



Wątki
Chcąc mieć 100% kontrolę nad wątkami, możemy skorzystać z klasy Thread. Podstawowe operacje w przypadku pracy z wątkami:
1. Tworzenie wątku:

Code:
            var thread = new Thread(() =>
                                        {
                                            //...Do something
                                        });
            thread.Start();

2. Niszczenie wątku:
Wątek można zniszczyć za pomocą metody Abort. Wywołanie tej metody na działającym wątku spowoduje rzucenie wyjątku ThreadAbortException:

Code:
            try
            {
                thread.Abort();
            }
            catch (ThreadAbortException e)
            {
                Console.WriteLine(e);
            }

3. Synchronizacja wątków
Zakleszczenia (deadlock) - występują gdy procesy czekają nawzajem na siebie - żaden nie jest w stanie ukończyć pracy
Sytuacje wyścigu (race conditions) - występują w momencie gdy więcej niż jeden proces odwołuje się do niezabezpieczonej pamięci współdzielonej
Do synchronizacji możemy wykorzystać słowo kluczowe lock:

Code:
            lock (aobj)
            {
                
            }



Wątki a kontrolki
Kontrolki na formatce obsługuje wątek nazywany UI Thread. Dostęp do kontrolek z innych wątków nie jest bezpieczny. Windows Forms jak i WPF definiują wzorce dostępu do wątku interfejsu:
Windows Forms:
Za pomocą delegaty odwołujemy się do wątku interfejsu:

Code:
        public delegate void SetTextDelegate(string t);
        public void SetText(string text)
        {
            if (textBox1.InvokeRequired)
            {
                var del = new SetTextDelegate(SetText);
                del.Invoke(text);
            }
            else
            {
                textBox1.Text = text;
            }
        }

WPF:
Za pomocą obiektu Dispatcher, wykonujemy zmiany w interfejsie:

Code:
            Dispatcher.Invoke(() => { });
            Dispatcher.BeginInvoke(() => { }, DispatcherPriority.Normal);

Pierwsza metoda jest blokująca. Druga nie zablokuje wątku interfejsu w czasie jego aktualizacji.

Brak komentarzy:

Prześlij komentarz