La classe System.Threading.Task.Task è una classe che rappresenta un'operazione asincrona, o meglio, un wrapper di un delegate contenente tutte le informazioni necessarie per conoscere lo stato di avanzamento di un’operazione, ad esempio se è terminata perchè completata o interrotta dall’utente, mediante le proprietà IsCompleted e IsCanceled o per interrompere l'esecuzione dell'operazione, chiamando l’apposito metodo Cancel. Essendo la classe Task priva di costruttori pubblici, per crearne un'istanza è necessario utilizzare uno degli overload del metodo statico Create. Per attendere il termine di un'operazione asincrona, un'istanza della classe Task mette a disposizione il metodo Wait (o meglio tre overload): Wait() senza parametri che attende la conclusione delle operazioni, Wait (TimeSpan span) che accetta un oggetto TimeSpan per indicare il tempo di attesa in millisecondi per il completamento delle operazioni ed infine Wait (int milliseonds) che accetta il numero di millisecondi per cui si vuole attendere la conclusione delle operazioni. Possiamo vedere immediatamente la classe Task in azione con un piccolo esempio. Supponiamo di avere la seguente funzione il cui scopo è creare un file di testo contenente un certo numero di righe, secondo il valore degli argomenti passati:
1 private static void WriteFile(IsolatedStorageFile storage, int start, int count)
2 {
3 string fileName = string.Format("start_{0}.txt", start);
4
5 lock (storage)
6 {
7 using (IsolatedStorageFileStream sw = new IsolatedStorageFileStream(fileName, System.IO.FileMode.OpenOrCreate, storage))
8 {
9 using (System.IO.StreamWriter swriter = new System.IO.StreamWriter(sw))
10 {
11 for (int i = start; i < start + count; i++)
12 swriter.WriteLine("Item#{0}", i);
13 }
14 }
15 }
16 }
Supponiamo di voler scrivere 5 file ed attendere per il completamento delle operazioni, possiamo scrivere qualcosa del tipo:
1 Task tFile1 = Task.Create(delegate { WriteFile(storage, 1, 10); });
2 Task tFile2 = Task.Create(delegate { WriteFile(storage, 11, 10); });
3 Task tFile3 = Task.Create(delegate { WriteFile(storage, 21, 10); });
4 Task tFile4 = Task.Create(delegate { WriteFile(storage, 31, 10); });
5 Task tFile5 = Task.Create(delegate { WriteFile(storage, 41, 10); });
6 Console.WriteLine("Writing...");
7 //Attende il completamento di tutti i task,
8 Task.WaitAll(new Task[] { tFile1, tFile2, tFile3, tFile4, tFile5 });
Invece di richiamare il metodo Wait (o uno dei suoi overload per ogni istanza di Task, possiamo richiamare il metodo statico WaitAll (anch'esso definito con tre overload) che accetta come primo parametro un vettore di oggetti Task (o da essi derivati), per i quali attendere il completamento delle operazioni. Fin qui tutto bene, ma il metodo WriteFile, esegue l'operazione senza ritornare nessun valore. Supponiamo allora di voler eseguire la scansione "parallela" di un certo numero di directory su disco per ottenere il numero di file di testo (*.txt) contenuti nelle directory. Una funzione molto semplice potrebbe essere di questo tipo:
1 public static int ScanDirectory( string path, string searchPattern)
2 {
3 return System.IO.Directory.GetFiles(path, searchPattern).Count();
4 }
In questo caso, a meno di creare una classe personalizzata che derivi da Task, possiamo utilizzare la classe Future<T>, che deriva da Task, e che permette il recupero di un valore di ritorno da un'operazione asincrona. Semplicemente possiamo scrivere:
1 Future<int> tSearch1 = Future.Create(() => ScanDirectory(@"C:\", "*.txt"));
2 Future<int> tSearch2 = Future.Create(() => ScanDirectory(@"C:\DIR1", "*.txt"));
3 Future<int> tSearch3 = Future.Create(() => ScanDirectory(@"C:\DIR2", "*.txt"));
4 Task.WaitAll(new Future<int>[] { tSearch1, tSearch2, tSearch3 });
Per quanto detto precedentemente, anche in questo caso è possibile utilizzare il metodo statico WaitAll per attendere il completamento delle operazioni. Per recuperare il numero di file di testo trovati nelle directory, accediamo alla proprietà Value di ogni istanza di Future<int>:
1 int filesCount = tSearch1.Value + tSearch2.Value + tSearch3.Value;
2 Console.WriteLine("Files Count:" + filesCount);
Ci sono delle piccole osservazioni da fare. Supponiamo di modificare la funzione ScanDirectory in questo modo:
1 public static int ScanDirectory( string path, string searchPattern)
2 {
3 System.Threading.Thread.Sleep(3000);
4 return System.IO.Directory.GetFiles(path, searchPattern).Count();
5 }
e di eseguire il codice seguente:
1 Future<int> tSearch1 = Future.Create(() => ScanDirectory(@"C:\Lavoro", "*.txt"));
2 tSearch1.Wait(1000);
3 int filesCount = 0;
4 filesCount = tSearch1.Value;
5 Console.WriteLine("Files Count:" + filesCount);
Supponendo che la directory “C:\Lavoro” contenga 4 file di testo, ci si potrebbe aspettare che il valore visualizzato sulla console sia zero (dato che Wait attende per solo un secondo), invece, verrà visualizzato il valore corretto, dato che l’accesso alla proprietà Value sì comporta come Wait senza parametri. Si potrebbe allora pensare di utilizzare il metodo Cancel(), subito dopo il Wait, ma verrebbe sollevata un'eccezione quando si tenta di recuperare il valore da Value:
1 tSearch1.Wait(1000);
2 tSearch1.Cancel();
Una possibile soluzione è quella di sfruttare le proprietà IsCanceled (o in casi diversi IsCompleted), in questo modo:
1 if(!tSearch1.IsCanceled ) filesCount = tSearch1.Value;
La semplificazione introdotta da queste classi per la gestione delle operazioni asincrone è veramente impressionante.