Un piccolo problema risolvibile (volendo) utilizzando le Microsoft Parallel Extensions per il .Net Framework. Supponiamo di avere un testo HTML contenente un certo numero di collegamenti a file di cui si vuole effettuare il download. Per ottenere una lista dei link, possiamo ad esempio utilizzare la classe Regex ed un'opportuna Regular Expression:
Regex reg = new Regex(regular_expression_pattern);
MatchCollection matches = reg.Matches(pageContent);
Ottenuta la collezione matches , possiamo seguire diverse strade per eseguire il download dei file, utilizzando istanze di System.Net.WebClient.
1) Eseguiamo un ciclo for e sequenzialmente eseguiamo il download dei file;
2) Scriviamo del codice per l'esecuzione su più thread utilizzando unThreadPool
3) Utilizziamo le Microsoft Parallel Extensions per il .Net Framework 3.5 (CTP 2008 al momento), scaricabili qui.
Sinceramente qualche settimana fa avrei utilizzato il secondo approccio, ma dato che le Parallel Extensions forniscono classi e metodi che semplificano la parallelizzazione del codice senza entrare nel dettaglio di thread e pool di thread, mi è sembrata una buona occasione per sperimentarle. Dovendo eseguire un ciclo sugli oggetti System.Text.RegularExpressions.Match presenti nella collezione matches, possiamo utilizzare il metodo Parallel.ForEach, il quale implementa l'esecuzione parallela dello statement foreach. Dovendo condividere alcune informazioni con i vari thread, come la directory dove depositare il file una volta scaricato e l'indirizzo base del sito (come ad esempio www.contoso.com) necessario alla ricostruzione completa dell'url da cui prelevare i file, invece di seguire strane alchimie, possiamo utilizzare un oggetto ParallelState o meglio ParallelState<DownloadStartInfo>, dove la classe DownloadStartInfo è così definita:
1 private class DownloadStartInfo
2 {
3 private string _baseUrl = "";
4 private string _directoryTarget = "";
5
6 public DownloadStartInfo(string baseUrl, string directoryTarget)
7 {
8 _baseUrl = baseUrl;
9 _directoryTarget = directoryTarget;
10 }
11
12 public string BaseUrl
13 {
14 get { return _baseUrl; }
15 set { _baseUrl = value; }
16 }
17
18 public string DirectoryTarget
19 {
20 get { return _directoryTarget; }
21 set { _directoryTarget = value; }
22 }
23 }
L'istanza di oggetto DownloadStartInfo sarà memorizzata all'interno della proprietà ParallelState.ThreadLocalState. Non resta quindi che scrivere il codice della funzione utilizzata per eseguire il Download dei file:
1 private static void DownloadFile(Match m, int i, ParallelState<DownloadStartInfo> ps)
2 {
3
4 try
5 {
6 if (!System.IO.Directory.Exists(ps.ThreadLocalState.DirectoryTarget))
7 System.IO.Directory.CreateDirectory(ps.ThreadLocalState.DirectoryTarget);
8
9 string relativeUrl = m.Groups["relativeUrl"].Value;
10 string fileName = m.Groups["fileName"].Value;
11 string address = string.Format("{0}{1}/{2}", ps.ThreadLocalState.BaseUrl , relativeUrl, fileName);
12
13 System.Net.WebClient client = new System.Net.WebClient();
14
15 string fullFilePath = System.IO.Path.Combine(ps.ThreadLocalState.DirectoryTarget, fileName);
16
17 Console.WriteLine("\t\tDownloading {0}", fileName);
18 client.DownloadFile(address, fullFilePath );
19 Console.WriteLine("\t\tDownload of {0} completed", fileName);
20
21 System.Threading.Thread.Sleep(150);
22 }
23 catch (System.Net.WebException wex)
24 {
25 Console.WriteLine("Error ==> {0}", wex.Message);
26 }
27 catch (Exception ex)
28 {
29 Console.WriteLine("Error ==> {0}", ex.Message);
30 }
31 }
Mettiamo tutto insieme scrivendo il codice relativo al Parallel.ForEach<>:
1 IEnumerable<Match> ms = matches.Cast<Match>();
2
3 Parallel.ForEach<Match, DownloadStartInfo>(ms,
4 delegate(){return new DownloadStartInfo("http://www.contoso.com", directoryTarget);},
5 delegate(Match m, int i, ParallelState<DownloadStartInfo> ps) { DownloadFile(m, i, ps); });
dove il primo Delegate indica la funzione che serve a generare i dati thread-Local (a cui si accede utilizzando l'oggetto ParallelState<...>) utilizzati localmente dai vari thread, mentre il secondo Delegate specifica il codice da richiamare ad ogni iterazione del Parallel.ForEach<...,..>. Oltre al metodo Parallel.ForEach, possiamo trovare anche il metodo Parallel.For, questi sono molto simili tra loro, ma entrambi richiedono che l'operazione eseguita da cisascun ciclo sia indipendente dagli altri. Piccola osservazione, per eseguire il codice, è necessarion aggiungere un riferimento all'assembly System.Threading.dll