Problema
Nello sviluppare l'esecuzione di un operazione asincrona con l'ausilio dei nuovi operatori async e await mi sono trovato in una situazione in cui avevo bisogno di cancellare o comunque invalidare una precedente operazione avviata. 
In particolare il task asincrono consisteva nel validare la connessione ad un database SQL Server via ADO.NET, si voleva che da un apposita form di configurazione, tutte le volte fosse modificata la stringa, alla perdita del fuoco fosse verificata la connessione e quindi colorato lo sfondo di verde oppure di rosso a secondo che la stringa fosse valida o meno. Essendo il progetto sviluppato in VS. 2012 (con l'ausilio di .NET 4.5) ho deciso di dilettarmi nell'utilizzare gli operatori async e await.
Il problema è questo: cosa succede se l'utente modifica prima la stringa con un nome di server errato poi si accorge e quindi corregge il nome del server.
In effetti senza nessun particolare accorgimento il risultato che si ha è il seguente: quando l'utente digita la stringa corretta, pochi istanti dopo il task di verifica termina e la casella viene colorata di verde, ma essendo in esecuzione un precedente task con una stringa non valida, quando quest'ultimo termina perché la richiesta di connessione va in timeout, successivamente la casella viene erroneamente colorata di rosso.
Di seguito riporto un test che esemplifica il problema
Caso che evidenzia il problema
    public
						class
								MultipleAsyncOps
    {
        readonly
												List<Task>
																	m_tsks
																			=
																					new
																							List<Task>();
        public
																														bool
																																TestResult { get; private
																																						set; }
        public
																																										void
																																												Test(bool
																																																input)
        {
            TestAsync(input);
        }
        async
																																																								void
																																																										TestAsync(bool
																																																														input)
        {
            var
																																																																		tsk
																																																																				=
																																																																						Task.Run(() =>
																																																																												CheckTest(input));
            m_tsks.Add(tsk);
            TestResult
																																																																																								=
																																																																																										await
																																																																																												tsk;
        }
        bool
																																																																																																CheckTest(bool
																																																																																																				input)
        {
            if (!input)
                Thread.Sleep(100); //Simulate error with timeout
            Console.WriteLine("Test completed - result: "
																																																																																																																									+
																																																																																																																											input);
            return
																																																																																																																															input;
        }
        public
																																																																																																																																			void
																																																																																																																																					WaitForCompletion()
        {
            m_tsks.ForEach(tsk
																																																																																																																																													=>
																																																																																																																																															tsk.Wait());
        }
    }
    [TestFixture]
    public
																																																																																																																																																							class
																																																																																																																																																									TestMultipleAsyncOps
    {
        [Test]
        public
																																																																																																																																																															void
																																																																																																																																																																	WhenMultileOperationsCompletedTheResultsIsFromLastOperation()
        {
            var
																																																																																																																																																																					mao
																																																																																																																																																																							=
																																																																																																																																																																									new
																																																																																																																																																																											MultipleAsyncOps();
            mao.Test(false);
            mao.Test(true);
            mao.WaitForCompletion();
            Assert.IsTrue(mao.TestResult); //Failed
        }
    }
	
NOTA: Il membro di classe m_tsks e il metodo WaitForCompletion() servono solo ai fini della sincronizzazione per i test.
Il test fallisce e sulla console viene prodotto il seguente outupt:
Test completed - result: True
Test completed - result: False
 
  Expected: True
  But was:  False
 
Soluzione – Utilizzo della classe CancellationTokenSource
	
Per risolvere questo problema si può utilizzare la classe CancellationTokenSource (http://msdn.microsoft.com/en-us/library/dd321629.aspx) attraverso il quale passare un token di cancellazione al task e segnalare quindi che l'operazione è stata cancellata.
Soluzione con CancellationTokenSource e TokenSource passato all'operazione
| public
										class
												MultipleAsyncOpsV2 {
 readonly
										List<Task>
															m_tsks
																	=
																			new
																					List<Task>();
 CancellationTokenSource
										m_cts;
 
 public
										bool
												TestResult { get; private
																		set; }
 
 public
										void
												Test(bool
																input)
 {
 TestAsync(input);
 }
 
 async
										void
												TestAsync(bool
																input)
 {
 if (m_cts
												!=
														null)
 m_cts.Cancel(); //Cancel previous operation
 
 m_cts
										=
												new
														CancellationTokenSource();
 var
										tsk
												=
														Task.Run(() =>
																				TestSync(input, m_cts.Token));
 
 m_tsks.Add(tsk);
 
 var
										result
												=
														await
																tsk;
 
 if (result.HasValue)
 TestResult
										=
												result.Value; //if result has value the task is not cancelled
 }
 
 bool?
											TestSync(bool
															input, CancellationToken
																			tk)
 {
 bool
										result
												=
														CheckTest(input);
 
 if (tk.IsCancellationRequested)
 {
 Console.WriteLine("Cancellation requested");
 return
										null;
 }
 
 Console.WriteLine("Test completed - result: "
														+
																input);
 
 return
										result;
 }
 
 bool
										CheckTest(bool
														input)
 {
 if (!input)
 Thread.Sleep(100); //Simulate error with timeout
 
 return
										input;
 }
 
 public
										void
												WaitForCompletion()
 {
 m_tsks.ForEach(tsk
														=>
																tsk.Wait());
 }
 }
 
 [TestFixture]
 public
										class
												TestMultipleAsyncOps
 {
 [Test]
 public
										void
												WhenMultileOperationsCompletedTheResultsIsFromLastOperationV2()
 {
 var
										mao
												=
														new
																MultipleAsyncOpsV2();
 mao.Test(false);
 mao.Test(true);
 mao.WaitForCompletion();
 Assert.IsTrue(mao.TestResult); //Ok
 }
 }
 | 
Il test ha esito positivo e sulla console viene prodotto il seguente outupt:
Test completed - result: True
Cancellation requested
 
Oltre a questo è possibile e consigliato (a mio avviso non in tutti i casi) passare il token di cancellazione all'operazione associata al task, il vantaggio è in termini di prestazioni e di carico del pool dei thread. In questo caso la cancellazione ha l'effetto di evitare l'avvio del task se questa è invocata dopo che il task è stato avviato ma prima che sia in esecuzione. Allora acquista significato il parametro throwOnFirstException, che determina il throw dell'eccezione OperationCanceledException. Per uniformare il comportamento è utile  all'interno del nostro task verificare la cancellazione e quindi generare l'eccezione OperationCanceledException. Se immaginiamo inoltre che il nostro task preveda un ciclo o un certo numero di iterazioni, potremo interrompere prima l'esecuzione migliorando le performance oppure passare ad un metodo asincrono innestato, che lo supporta, il Token di cancellazione.
Soluzione con CancellationTokenSource e TokenSource passato all'operazione e al task
|     public
										class
												MultipleAsyncOpsV3
													{ readonly
										List<Task>
															m_tsks
																	=
																			new
																					List<Task>();
 CancellationTokenSource
										m_cts;
 
 public
										bool
												TestResult { get; private
																		set; }
 
 public
										void
												Test(bool
																input)
 {
 TestAsync(input);
 }
 
 async
										void
												TestAsync(bool
																input)
 {
 if (m_cts
												!=
														null)
 m_cts.Cancel(true); //Cancel previous operation with exception
 
 m_cts
										=
												new
														CancellationTokenSource();
 var
										tsk
												=
														Task.Run(() =>
																				TestSync(input, m_cts.Token), m_cts.Token);
 
 m_tsks.Add(tsk);
 
 try
 {
 TestResult
										=
												await
														tsk;
 }
 catch (OperationCanceledException
												e)
 {
 Console.WriteLine("Test cancelled : "
														+
																e);
 }
 }
 
 bool
										TestSync(bool
														input, CancellationToken
																		tk)
 {
 bool
										result
												=
														CheckTest(input);
 
 if (tk.IsCancellationRequested)
 throw
										new
												OperationCanceledException(tk);
 
 Console.WriteLine("Test completed - result: "
														+
																input);
 
 return
										result;
 }
 
 bool
										CheckTest(bool
														input)
 {
 if (!input)
 Thread.Sleep(100); //Simulate error with timeout
 
 return
										input;
 }
 
 public
										void
												WaitForCompletion()
 {
 foreach (var
												tsk
														in
																m_tsks)
 {
 try
 {
 tsk.Wait();
 }
 catch (AggregateException)
 {
 }
 }
 }
 }
 
 [TestFixture]
 public
										class
												TestMultipleAsyncOps
 {
 [Test]
 public
										void
												WhenMultileOperationsCompletedTheResultsIsFromLastOperationV3()
 {
 var
										mao
												=
														new
																MultipleAsyncOpsV3();
 mao.Test(false);
 mao.Test(true);
 mao.WaitForCompletion();
 Assert.IsTrue(mao.TestResult); //Ok
 }
     } | 
Il test ha esito positivo e sulla console viene prodotto il seguente outupt:
Test completed - result: True
Test cancelled : System.OperationCanceledException: The operation was canceled.
   at ClassLibrary1.MultipleAsyncOpsV3.TestSync(Boolean input, CancellationToken tk) in c:\TEMP\ClassLibrary1\ClassLibrary1\MultipleAsyncOpsV3.cs:line 48
   at ClassLibrary1.MultipleAsyncOpsV3.<>c__DisplayClass1.<TestAsync>b__0() in c:\TEMP\ClassLibrary1\ClassLibrary1\MultipleAsyncOpsV3.cs:line 27
   at System.Threading.Tasks.Task`1.InnerInvoke()
   at System.Threading.Tasks.Task.Execute()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at ClassLibrary1.MultipleAsyncOpsV3.<TestAsync>d__3.MoveNext() in c:\TEMP\ClassLibrary1\ClassLibrary1\MultipleAsyncOpsV3.cs:line 33
 
Vedi anche