W .NET można anulować wątki za pomocą tokena. Oczywiście nie należy używać metody Abort czy Cancel, ale o tym już wiele razy pisałem – w skrócie nie wiadomo kiedy taki wątek zostanie przerwany. Użycie tokena jest proste tzn. (przykład MSDN):
class Program { static void Main() { var tokenSource2 = new CancellationTokenSource(); CancellationToken ct = tokenSource2.Token; var task = Task.Factory.StartNew(() => { // Were we already canceled? ct.ThrowIfCancellationRequested(); bool moreToDo = true; while (moreToDo) { // Poll on this property if you have to do // other cleanup before throwing. if (ct.IsCancellationRequested) { // Clean up here, then... ct.ThrowIfCancellationRequested(); } } }, tokenSource2.Token); // Pass same token to StartNew. tokenSource2.Cancel(); // Just continue on this thread, or Wait/WaitAll with try-catch: try { task.Wait(); } catch (AggregateException e) { foreach (var v in e.InnerExceptions) Console.WriteLine(e.Message + " " + v.Message); } Console.ReadKey(); } }
Należy sprawdzać czy flaga IsCancellationRequested jest ustawiona i wtedy odpowiednio zareagować. Daje nam to pełną kontrolę nad tym, kiedy wątek zakończy działanie.
Sprawa prosta. Ale co jeśli w naszej logice musimy czekać na jakieś inne wątki? Jeśli mamy obiekty synchronizacyjne wtedy sprawa nieco komplikuje się . Wyobraźmy sobie taką pętle:
while (true) { _event.Wait(); // czekaj na jakies zdarzenie // wykonanie pracy if (ct.IsCancellationRequested) { ct.ThrowIfCancellationRequested(); } }
Powyższy kod może być implementacją wzorca producent\konsument. Jeden wątek czeka na porcje danych a drugi generuje te dane. Każdy z nich ma ten sam token sterujący. Co jeśli producent prawidłowo zostanie anulowany a następnie powyższy wątek utknie na _event.Wait? Jeśli wspieramy anulowanie wątków musimy być szczególnie ostrożni, gdy używamy jakichkolwiek mechanizmów synchronizacji.
Jeśli nasz obiekt synchronizujący nie wspiera anulowania wtedy możemy skorzystać WaitHandle.WaitAny. Metoda ta czeka aż jakieś zdarzenia zostanie ustawione. Token również eksponuje WaitHandle, zatem możemy powyższy kod przepisać na:
int eventThatSignaledIndex = WaitHandle.WaitAny(new WaitHandle[] { _event, token.WaitHandle }, new TimeSpan(0, 0, 20));
Metoda zwraca indeks zdarzenia, które zostało odebrane. Jeśli zatem token ustawił zdarzenie to oczywiście przerywamy pracę. Pełny przykład (MSDN):
class CancelOldStyleEvents { // Old-style MRE that doesn't support unified cancellation. static ManualResetEvent mre = new ManualResetEvent(false); static void Main() { var cts = new CancellationTokenSource(); // Pass the same token source to the delegate and to the task instance. Task.Run(() => DoWork(cts.Token), cts.Token); Console.WriteLine("Press s to start/restart, p to pause, or c to cancel."); Console.WriteLine("Or any other key to exit."); // Old-style UI thread. bool goAgain = true; while (goAgain) { char ch = Console.ReadKey().KeyChar; switch (ch) { case 'c': cts.Cancel(); break; case 'p': mre.Reset(); break; case 's': mre.Set(); break; default: goAgain = false; break; } Thread.Sleep(100); } } static void DoWork(CancellationToken token) { while (true) { // Wait on the event if it is not signaled. int eventThatSignaledIndex = WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle }, new TimeSpan(0, 0, 20)); // Were we canceled while waiting? if (eventThatSignaledIndex == 1) { Console.WriteLine("The wait operation was canceled."); throw new OperationCanceledException(token); } // Were we canceled while running? else if (token.IsCancellationRequested) { Console.WriteLine("I was canceled while running."); token.ThrowIfCancellationRequested(); } // Did we time out? else if (eventThatSignaledIndex == WaitHandle.WaitTimeout) { Console.WriteLine("I timed out."); break; } else { Console.Write("Working... "); // Simulating work. Thread.SpinWait(5000000); } } } }
Cześć obiektów wspiera bezpośrednio tokeny i zamiast WaitAny możemy (przykład z MSDN):
static void DoWork(CancellationToken token) { while (true) { if (token.IsCancellationRequested) { Console.WriteLine("Canceled while running."); token.ThrowIfCancellationRequested(); } // Wait on the event to be signaled // or the token to be canceled, // whichever comes first. The token // will throw an exception if it is canceled // while the thread is waiting on the event. try { // mres is a ManualResetEventSlim mres.Wait(token); } catch (OperationCanceledException) { // Throw immediately to be responsive. The // alternative is to do one more item of work, // and throw on next iteration, because // IsCancellationRequested will be true. Console.WriteLine("The wait operation was canceled."); throw; } Console.Write("Working..."); // Simulating work. Thread.SpinWait(500000); } }
Po prostu metoda Wait przyjmuje jako parametr token do anulowania. W przypadku, gdy token zostanie ustawiony to Wait wyrzuci OperactionCanceledException.
Wniosek z wpisu jest taki, że przed użyciem tokenu należy szczególnie przyjrzeć się używanym metodom synchronizacyjnym ponieważ łatwo o zakleszczenie i wyciek pamięci.