Area di riferimento
- Implementing service processes, threading, and application domains in a .NET Framework application
- Develop multithreaded .NET Framework applications
- IAsyncResult interface (Refer System namespace)
The Asynchronous Programming Model (APM)
Eseguire operazioni in modo asincrono è la chiave per la costruzione di applicazioni scalabili e con alte performance. Il team di Microsoft del CLR ha progettato un pattern che rende semplice allo sviluppatore l'utilizzo della programmazione asincrona. Molte classi del framework supportano l'APM fornedo metodi del tipo BeginXXX e EndXXX. Ad esempio la classe FileStream ha un metodo Read che legge i dati dallo stream e fornisce i metodi BeginRead ed EndRead in modo da poter eseguire la stessa operazione in modo asincrono.
Eseguire operazioni di I/O in modo sincrono è molto inefficiente perchè il tempo necessario per effettuare l'operazione non è prevedibile e il thread chiamante viene sospeso e quindi diventa incapace di continuare il suo lavoro sprecando risorse. Il metodo BeginRead invece accoda una richiesta al driver del dispositivo di I/O che comunicherà direttamente all'hardware l'operazione da svolgere in modo da rendere libero il thread chiamante di continuare il suo lavoro.
Il modello APM supporta tre tecniche di rendezvous: wait-until-done, polling e method callback.
Wait-Until-Done
Per iniziare una operazione asincrona basta chiamare il metodo BeginXXX. Tutti questi metodi accodano l'operazione desiderata e ritornano un oggetto IAsyncResult che identifica la richiesta pendente. Per ottenere il risultato dell'operazione è possibile semplicemente chiamare il corrispondente metodo EndXXX passando come parametro l'oggetto IAsyncResult. Se l'operazione asincrona è stata completata quando viene chiamato il metodo EndXXX questo ritornerà immediatamente altrimenti il thread chiamante viene sospeso fino a quando l'operazione non sarà completata.
Ecco un esempio che mostra l'utilizzo di questa tecnica:
// Apro il file specificando che si desidera effettuare I/O in modo asincrono
FileStream fs = new FileStream(@"dati.dat", FileMode.Open, FileAccess.Read,
FileShare.Read, 1024, FileOptions.Asynchronous);
Byte[] buffer = new Byte[100];
// Inizia l'operazione di lettura asincrona
IAsyncResult ar = fs.BeginRead(buffer, 0, buffer.Length, null, null);
// Effettua qualche altra operazione...
// Sospende questo thread fino al completamento dell'operazione e ottiene i risultati
Int32 byteLetti = fs.EndRead(ar);
// Non ci sono altre operazioni quindi si chiude il file.
fs.Close();
// Visualizzazione dei risultati
Console.WriteLine("Numero di byte letti: {0}", byteLetti);
Console.WriteLine(BitConverter.ToString(buffer, 0, byteLetti));
Polling
L'interfaccia IAsyncResult è così definita:
[ComVisible(true)]
public interface IAsyncResult
{
// Properties
object AsyncState { get; }
WaitHandle AsyncWaitHandle { get; }
bool CompletedSynchronously { get; }
bool IsCompleted { get; }
}
La proprietà IsCompleted può essere utilizzata dallo sviluppatore utilizzando la tecnica del polling. Questa proprietà permette allo sviluppatore di sapere se l'operazione asincrona è stata completata ma senza bloccare il thread chiamante. Vediamo un esempio di utilizzo:
// Apro il file specificando che si desidera effettuare I/O in modo asincrono
FileStream fs = new FileStream(@"dati.dat", FileMode.Open, FileAccess.Read,
FileShare.Read, 1024, FileOptions.Asynchronous);
Byte[] buffer = new Byte[100];
// Inizia l'operazione di lettura asincrona
IAsyncResult ar = fs.BeginRead(buffer, 0, buffer.Length, null, null);
while (!ar.IsCompleted)
{
// Effettua qualche altra operazione...
}
// Ottiene i risultati dell'operazione asincrona (non è bloccante!)
Int32 byteLetti = fs.EndRead(ar);
// Non ci sono altre operazioni quindi si chiude il file.
fs.Close();
// Visualizzazione dei risultati
Console.WriteLine("Numero di byte letti: {0}", byteLetti);
Console.WriteLine(BitConverter.ToString(buffer, 0, byteLetti));
Method Callback
Questa è la tecnica migliore quando si progetta una applicazione per performance e scalabilità. Il motivo è che questa tecnica non causa mai la sospensione del thread chiamante (al contrario della tecnica wait-until-done) e perchè questa tecnica non spreca mai tempo di CPU per controllare periodicamente se l'operazione asincrona è stata completata ( al contrario della tecnica del polling ).
Per prima cosa la richiesta di I/O asincrona viene accodata e il thread continua a svolgere il proprio lavoro. Quando la richiesta di I/O termina, Windows accoda un work item all'interno del thread pool del CLR. Un thread del pool preleverà il work item e chiamerà un metodo che lo sviluppatore ha scritto; questo è il modo in cui si viene a conoscenza che l'operazione richiesta è stata completata. A questo punto all'interno del metodo di callback è possibile chiamare il metodo EndXXX per ottenere il risultato dell'operazione in modo che possa essere elaborato secondo la logica applicativa. Quando il metodo ritorna, il thread del pool diventa libero di servire ulteriori richieste.
Vediamo un esempio di codice che utilizza questa tecnica:
// Il buffer è statico così che possa essere acceduto dal Main e dal metodo ReadDone
private static Byte[] buffer = new Byte[100];
static void Main(string[] args)
{
// Apro il file specificando che si desidera effettuare I/O in modo asincrono
FileStream fs = new FileStream(@"dati.dat", FileMode.Open, FileAccess.Read,
FileShare.Read, 1024, FileOptions.Asynchronous);
// Inizia l'operazione di lettura asincrona
IAsyncResult ar = fs.BeginRead(buffer, 0, buffer.Length, fineLettura, fs);
// Altre operazioni...
Console.ReadKey();
}
static void fineLettura(IAsyncResult ar)
{
// Estrae il FileStream (lo stato) dell'oggetto IAsyncResult
FileStream fs = ar.AsyncState as FileStream;
// Ottiene il risultato dell'operazione
Int32 byteLetti = fs.EndRead(ar);
// Non ci sono altre operazioni quindi si chiude il file.
fs.Close();
// Visualizzazione dei risultati
Console.WriteLine("Numero di byte letti: {0}", byteLetti);
Console.WriteLine(BitConverter.ToString(buffer, 0, byteLetti));
}
Affinchè all'interno del Main e del metodo ReadDone sia possibile accedere al contenuto del buffer nell'esempio si è utilizzato un buffer statico. Questa scelta in pratica non dovrebbe essere fatta in quanto non permette estensioni future. E' più corretto procedere alla costruzione di una classe che contiene il buffer e il campo FileStream in modo da poter passare un riferimento ad una sua istanza come ultimo parametro del metodo BeginRead. Questo riferimento è infatti accessibile nel metodo di callback accedendo alla proprietà AsyncState dell'oggetto IAsyncResult. In particolare questa proprietà permette di ottenere un riferimento all'oggetto sul quale è stato eseguito il metodo BeginRead in modo da poter chiamare correttamente il metodo EndRead.
E' estremamente importante chiamare sempre il metodo EndXXX all'interno del metodo di callback per evitare uno spreco di risorse ma soprattutto per sapere se l'operazione richiesta è stata completata correttamente o meno. Nel caso in cui si sia verificato un errore la chiamata al metodo EndXXX solleverà una eccezione che andrà opportunamente gestita.
Note
Alcuni metodi BeginXXX potrebbero ritornare un oggetto che implementa l'interfaccia IAsyncResult e che offre qualche tipo di metodo cancel. In questo caso significa che il thread corrente può cancellare l'operazione asincrona richiesta.
In Windows, una finestra è sempre creata da un thread e questo thread deve essere utilizzato per processare tutte le azioni per la finestra stessa (WM_MOVE, WM_PAINT, ecc.).
In quanto le Windows Forms sono costruite al di sopra di Windows, ad un thread non è permesso di accedere direttamente agli oggetti definiti nella form o in generale a una classe derivata da Control. Per questo motivo i controlli offrono tre metodi (Invoke, BeginInvoke e EndInvoke) in modo che possa essere effettuato il marshal di una operazione dal thread chiamante al thread che ha creato la finestra.