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)
{
}
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(() =>
{
});
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:
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.