lunedì 11 ottobre 2010

I Tasks del .NET Framework 4.0

In questo post vorrei porre l’attenzione su alcune classi, introdotte nel Framework 4.0, che semplificano la scrittura di codice multithread.

Si tratta delle classi per la gestione dei task cioè di operazioni asincrone su thread separati.

Queste classi fanno parte della Task Parallel Library (TPL) introdotta con il Framework 4.0 e si trovano all’interno del namespace è System.Threading.Task e sono, in dettaglio, le seguenti:

  • Task : Rappresenta un'operazione asincrona.
  • Task(Of TResult) : Rappresenta un'operazione asincrona in grado di restituire un valore.
  • TaskCanceledException : Rappresenta un'eccezione utilizzata per comunicare l'annullamento di un'attività.
  • TaskCompletionSource(Of TResult) : Rappresenta il lato producer di un oggetto Task(Of TResult) non associato a un delegato e che consente l'accesso al lato consumer tramite la proprietà Task.
  • TaskExtensions : Fornisce un set di metodi statici (Shared in Visual Basic) per l'utilizzo di tipi specifici di istanze di Task.
  • TaskFactory : Fornisce supporto per la creazione e la pianificazione di oggetti Task.
  • TaskFactory(Of TResult) : Fornisce supporto per la creazione e la pianificazione di oggetti Task(Of TResult).
  • TaskScheduler : Rappresenta un oggetto che gestisce le operazioni di basso livello relative all'accodamento delle attività nei thread.
  • TaskSchedulerException : Rappresenta un'eccezione utilizzata per comunicare un'operazione non valida eseguita da un oggetto TaskScheduler.
  • UnobservedTaskExceptionEventArgs : Fornisce i dati dell'evento generato quando l'eccezione di un oggetto Task in cui si è verificato un errore non viene osservata.

Classi Task e Task(Of Result)

Entrambe le classi permettono la creazione e gestione di attività multithread asincrone. La classe Task è utilizzata per attività che non hanno la necessità di restituire un valore mentre la Task(Of Result) può essere utilizzata quando dobbiamo eseguire attività asincrone al cui termine abbiamo la necessità di un valore di ritorno. La Task(Of Result) deriva dalla Task.

Le due classi mettono a disposizione una serie di costruttori che ci permettono poter eseguire task in tantissime modalità differenti.

Cominciamo a vedere qualche esempio. Negli esempi riportati di seguito alterneremo la classe Task e la classe Task(Of Result) in modo da far vedere le funzionalità di tutte e due.

Primo esempio molto semplice prevede l’esecuzione di una Sub denominata LongRunJob e l’attesa da parte del programma principale:

  1. Private Sub SimpleTask()
  2.     Dim task = New Threading.Tasks.Task(New Action(AddressOf LongRunJob))
  3.     Console.WriteLine("Start LongRunJob")
  4.     task.Start()
  5.     task.Wait()
  6.     Console.WriteLine("End LongRunJob")
  7.     Console.ReadLine()
  8. End Sub
  9.  
  10. Private Sub LongRunJob()
  11.     Console.WriteLine("{0:HH.mm.ss} - LongRunJob Iniziato", DateTime.Now)
  12.     Threading.Thread.Sleep(10000)
  13.     Console.WriteLine("{0:HH.mm.ss} - LongRunJob Completato", DateTime.Now)
  14. End Sub

In questo esempio è creato un oggetto di classe Task a cui è passato il delegate del codice da eseguire (LongRunJob), è eseguito il task (cioè il codice all’interno della sub LongRunJob) e atteso il completamento dello stesso. Il metodo Start della classe Task (e della classe Task(Of Result)) prevede due possibili firme: la prima è quella dell’esempio precedente mentre la seconda permette la gestione di task schedulati.

Nell’esempio precedente abbiamo atteso il completamento del task incondizionatamente ma possiamo, se serve, attendere per un determinato timeout. Nel successivo esempio vediamo come utilizzare un task che restituisce un valore e come attendere il completamento per un determinato timeout:

  1. Private Sub ResultTask()
  2.     Dim sleepTime = New Random(DateTime.Now.Millisecond).Next(10000)
  3.     Dim task = New Threading.Tasks.Task(Of DateTime)(Function()
  4.                                                          Return LongRunJob(sleepTime)
  5.                                                      End Function)
  6.     Console.WriteLine("Start LongRunJob")
  7.     task.Start()
  8.     If Not task.Wait(5000) Then
  9.         Console.WriteLine("Timeout scattato")
  10.     Else
  11.         Dim result = task.Result
  12.         Console.WriteLine("End LongRunJob- {0:HH.mm.ss}", result)
  13.     End If
  14.  
  15.     Console.ReadLine()
  16. End Sub
  17.  
  18. Private Function LongRunJob(ByVal sleepTime As Integer) As DateTime
  19.     Console.WriteLine("{0:HH.mm.ss} - LongRunJob Iniziato", DateTime.Now)
  20.     Threading.Thread.Sleep(sleepTime)
  21.     Console.WriteLine("{0:HH.mm.ss} - LongRunJob Completato", DateTime.Now)
  22.     Return DateTime.Now
  23. End Function

In questo esempio è avviato un task composto da una funzione che accetta un intero (numero di millisecondi di attesa) e restituisce una data (data di completamento). Questa volta il costruttore del task utilizza una espressione lambda per definire cosa fa il task stesso. Per attendere il completamento del task, viene utilizzata la firma del metodo Wait() che prevede un numero di millisecondi di attesa. Scaduto il timeout, il metodo Wait() restituisce false. Nell’esempio precedente, se non scade il timeout, viene recuperato il valore di ritorno attraverso la proprietà Result.

Le due classi Task prevedono alcune proprietà utili:

  • AsyncState : Ottiene l'oggetto stato fornito quando è stato creato Task oppure null se non ne è stato fornito alcuno. L’oggetto AsyncState ha la stessa funzionalità del corrispondente per i thread classici;
  • CreationOptions : Ottiene l'oggetto TaskCreationOptions utilizzato per creare questa attività. L’utilizzo di tale proprietà verrà spiegato in un esempio successivo;
  • CurrentId : Restituisce l'ID univoco dell'oggetto Task attualmente in esecuzione;
  • Exception : Ottiene l'oggetto AggregateException che ha causato l'interruzione anomala dell'oggetto Task.Se l'oggetto Task è stato completato correttamente o non ha ancora generato alcuna eccezione, verrà restituito Nothing;
  • Factory : Fornisce l'accesso ai metodi factory per la creazione di istanze di Task e Task(Of TResult);
  • Id : Ottiene un ID univoco per questa istanza di Task;
  • IsCanceled : Ottiene un valore che indica se l'esecuzione di questa istanza di Task è stata completata poiché è stata annullata;
  • IsCompleted : Ottiene un valore che indica se questo oggetto Task è stato completato;
  • IsFaulted : Ottiene un valore che indica se l'oggetto Task è stato completato a causa di un'eccezione non gestita;
  • Status : Ottiene l'oggetto TaskStatus di questa attività.

Come possiamo osservare, ogni Task dispone di un ID univoco assegnato dal sistema che ci permette di caratterizzarlo e riconoscerlo.

Nel precedente esempio abbiamo visto come attendere il completamento di un task per un certo timeout. Osserviamo che, nel momento in cui il Wait() ritorna con risultato False (cioè il timeout è scaduto) il task non viene automaticamente interrotto e continua la sua opera. Se abbiamo la necessità di interromperlo, dobbiamo ricorrere ad un CancellationToken nel seguente modo:

  1. Private Sub ResultTaskAborted()
  2.     Dim sleepTime = New Random(DateTime.Now.Millisecond).Next(10000)
  3.     Dim tokenSource = New CancellationTokenSource()
  4.     Dim cancToken = tokenSource.Token
  5.  
  6.     Dim task = New Threading.Tasks.Task(Of DateTime)(Function()
  7.                                                          Return LongRunJob(sleepTime, cancToken)
  8.                                                      End Function, cancToken)
  9.     Console.WriteLine("Start LongRunJob")
  10.     task.Start()
  11.     If Not task.Wait(5000) Then
  12.         tokenSource.Cancel()
  13.         Console.WriteLine("Timeout scattato")
  14.     Else
  15.         Dim result = task.Result
  16.         Console.WriteLine("End LongRunJob- {0:HH.mm.ss}", result)
  17.     End If
  18.     Console.ReadLine()
  19. End Sub
  20.  
  21. Private Function LongRunJob(ByVal sleepTime As Integer, ByVal token As CancellationToken) As DateTime
  22.     Dim startTime = DateTime.Now
  23.     Console.WriteLine("{0:HH.mm.ss} - LongRunJob Iniziato", startTime)
  24.     While (DateTime.Now.Subtract(startTime).TotalMilliseconds <= sleepTime)
  25.         If token.IsCancellationRequested Then
  26.             Return DateTime.MinValue
  27.         End If
  28.         Threading.Thread.Sleep(25)
  29.     End While
  30.     Console.WriteLine("{0:HH.mm.ss} - LongRunJob Completato", DateTime.Now)
  31.     Return DateTime.Now
  32. End Function

Il token di annullamento è generato utilizzando una “sorgente di token” (classe CancellationSourceToken) la quale ci fornisce anche il metodo per richiedere la cancellazione (metodo Cancel()). Il codice del nostro task controlla l’eventuale richiesta di annullamento (IsCancellationRequest=true) e a seguito di questo chiude il proprio lavoro.

Se il nostro task vuole comunicare al mondo esterno che sta chiudendo in maniera anomala, si può utilizzare il metodo ThrowIfCancellationRequested() che è equivalente a generare un eccezione di tipo OperationCanceledException nei thread che sono in waiting sul task.

Il token di annullamento può essere utilizzato anche in altre tipologie di oggetti quali Thread o BackgroundWorker (per questo motivo si trova all’interno di System.Thread e non di System.Thread.Tasks).

Nel costruttore della classe Task (o Task(Of Result)) possiamo utilizzare anche l’enumerazione TaskCreationOptions che ci consente di definire il comportamento delle nostre attività. I valori possibili per l’enumerazione TaskCreationOptions sono i seguenti:

  • None : Specifica che deve essere utilizzato il comportamento predefinito;
  • PreferFairness : Indicazione fornita a un oggetto TaskScheduler affinché pianifichi un'attività nel modo più giusto possibile, ovvero garantendo che le attività pianificate prima abbiano più possibilità di essere eseguite prima delle attività pianificate in un secondo momento;
  • LongRunning : Specifica che un'attività sarà un'operazione a bassa precisione di lunga durata. Fornisce a TaskScheduler un'indicazione in merito alla possibilità di dover ricorrere all'oversubscription;
  • AttachedToParent : Specifica che un'attività è connessa a un elemento padre nella gerarchia delle attività.

Se vogliamo,quindi, far eseguire il nostro task LongRunJob come un task a lunga durata, ci basta scrivere:

  1. Dim task = New Threading.Tasks.Task(New Action(AddressOf LongRunJob), Threading.Tasks.TaskCreationOptions.LongRunning)

L’enumerazione TaskCreationOptions è un enumerazione decorate con l’attributo Flag() e, quindi, è una enumerazione il cui valore può essere anche una combinazione dei valori indicate nella precedente tabella.

Classi TaskFactory e TaskFactory(Of Result)

Le classi TaskFactory e TaskFactory(Of Result) forniscono dei metodi che permettono la creazione dei task:

  1. Private Sub SimpleFactoryTask()
  2.     Console.WriteLine("Start LongRunJob")
  3.     Dim factory = New Threading.Tasks.TaskFactory
  4.     Dim task = factory.StartNew(New Action(AddressOf LongRunJob))
  5.     task.Wait()
  6.     Console.WriteLine("End LongRunJob")
  7.     Console.ReadLine()
  8. End Sub

Nel precedente esempio, tramite la classe TaskFactory, creiamo e avviamo il nostro task.

Classe TaskScheduler

Una delle funzionalità più interessanti dei tasks di .NET Framework 4.0 è la possibilità di realizzare in maniera molto semplice uno scheduler delle attività gestito quasi completamente dal framework. Per fare questo ci viene in aiuto la classe TaskScheduler che rappresenta la classe base su cui lavorare per creare il nostro gestore di attività.

Derivando la classe TaskScheduler e implementando opportunamente i metodi della stessa possiamo realizzare il nostro gestore delle attività. Un esempio di implementazione è riportato all’indirizzo http://msdn.microsoft.com/it-it/library/ee789351.aspx in cui viene mostrato come realizzare uno scheduler che limita il grado di concorrenza sulle attività.

Nessun commento: