Metodologie di Programmazione: Lezione 14

Riccardo Silvestri

Web e computazioni asincrone

Scaricare risorse dal Web (pagine, immagini, ecc.) è una delle tipiche situazioni per cui l'esecuzione multithreading porta a notevoli miglioramenti delle prestazioni. Questo perché ogni task, cioè lo scaricare una singola risorsa, è indipendente dagli altri e consiste prevalentemente di tempo d'attesa, attesa che il server remoto risponda. E i tempi d'attesa possono essere "eseguiti" in parallelo persino da un singolo processore e anche in parallelo con una vera esecuzione, una che usa la CPU. Un task eseguito in modo indipendente da altre esecuzioni è anche chiamato computazione asincrona. La libreria di Java offre strumenti molto versatili ed utili per l'esecuzione di computazioni asincrone. La vedremo all'opera implementando una piccola applicazione che può interrogare più servizi web simultaneamente.

Web

Il package java.net della libreria Java offre strumenti potenti per usare una rete. In questa lezione useremo solamente alcuni di quelli a più alto livello che riguardano URL e connessioni a server remoti.

URL

La classe URL, in java.net, rappresenta un Uniform Resource Locator, cioè l'indirizzo di una risorsa nel World Wide Web. Tipicamente la risorsa è una pagina in HTML ma può essere molte altre cose un immagine, un video, un PDF, ecc. Un oggetto URL può essere creato tramite il costruttore URL(String spec) throws MalformedURLException dove la stringa spec è interpretata secondo la sintassi:

<scheme>://<authority><path>?<query><fragment>

Se spec non può essere interpretata secondo tale sintassi, il costruttore lancia l'eccezione dichiarata. Dopo aver costruito un oggetto URL, relativo all'indirizzo che ci interessa, si potrebbe usare uno dei due metodi seguenti per scaricare la risorsa: Object getContent() throws IOException o InputStream openStream() throws IOException. Ma questi metodi non permettono di impostare alcuna proprietà della richiesta. Per scaricare una pagina bisogna effettuare una richiesta a un server (il cui indirizzo è contenuto nell'URL). La richiesta è effettuata tramite il protocollo HTTP e comprende parecchie informazioni oltre all'URL, come ad esempio, il formato accettato (ad es. text/html), le codifiche del contenuto (ad es. gzip), le codifiche dei caratteri (ad es. UTF-8), e molte altre. Se queste informazioni non vengono fornite nella richiesta, il server può assumere che il client, cioè, chi effettua la richiesta, accetta qualsiasi formato, codifica, ecc. Quindi, quasi sempre occorre impostare alcune di queste proprietà ma la classe URL non permette di farlo. Occorre ottenere da URL un'altro oggetto che rappresenta una connessione.

Connessioni

Un oggetto URLConnection, sempre in java.net, rappresenta una connessione a un server remoto, e può essere ottenuto tramite il seguente metodo di un oggetto URL.

URLConnection openConnection() throws IOException
Ritorna un oggetto URLConnection che rappresenta la connessione al server remoto relativo a questo URL. La connessione non è stabilita quando il metodo è invocato ma solo quando è invocato un metodo di URLConnection che richiede esplicitamente che la connessione sia stabilita.

Dopo aver ottenuto un oggetto URLConnection si possono impostare le proprietà tramite il metodo
void setRequestProperty(String key, String value)
Imposta la proprietà key della richiesta con il valore value.

Oltre alle proprietà della richiesta si possono voler impostare altre proprietà. Tra le più utili ci sono quelle relative ai massimi tempi di attesa (timeout). I timeout di default sono impostati al valore 0 che significa che non c'è alcun timeout. Per impostare il timeout per l'apertura della connessione si deve usare il metodo void setConnectTimeout(int timeout) e per impostare il timeout per la lettura si deve usare void setReadTimeout(int timeout). In entrambi i casi il timeout è espresso in millisecondi.

Dopo quindi aver impostato tutte le proprietà volute possiamo aprire la connessione e iniziare a scaricare la risorsa. Si potrebbe pensare di usare il metodo Object getContent() throws IOException, ma per interpretare l'oggetto ritornato bisognerebbe usare delle librerie che non sono standard. Perciò conviene usare invece i seguenti metodi in successione.

void connect() throws IOException
Apre una connessione alla risorsa relativa all'URL di questa URLConnection. Lancia una SocketTimeoutException se la connessione non è stabilita entro il timeout impostato. Lancia IOException se accade un errore di I/O durante l'apertura della connessione.
InputStream getInputStream() throws IOException
Ritorna un flusso di input per leggere da questa connessione aperta. Lancia SocketTimeoutException se durante la lettura dal ritornato flusso una richiesta di lettura non è esaudita entro il timeout impostato. Lancia IOException se accade un errore di I/O durante la creazione del flusso di input.
Si noti che il flusso di input ritornato è dello stesso tipo di quello di System.in.

Finalmente abbiamo tutti gli strumenti necessari per poter scrivere un metodo che legge una pagina html dal Web. Ci manca ancora un ultimo strumento, un metodo che ci permetta di leggere l'intero contenuto da un flusso di input (cioè un InputStream) in una stringa. Purtroppo non ci sono metodi già pronti nella libreria standard. Così dobbiamo scriverlo noi. Questo ed altri metodi potranno risultare utili anche per altre situazioni che hanno a che fare con il Web, perciò decidiamo di introdurre un nuovo package mp.web e di creare in esso una classe di metodi di utilità chiamata Utils.

package mp.web;

/** Metodi di utilità per il Web */
public class Utils {
    /** Ritorna la stringa ottenuta dalla sequenza dei bytes del flusso dato
     * decodificando i caratteri tramite la codifica specificata. I fine linee
     * sono sostituiti con '\n'.
     * @param in  un flusso di bytes
     * @param cs  la codifica per i caratteri
     * @return una stringa che contiene i caratteri decodificati dal flusso */
    public static String read(InputStream in, Charset cs) {
        BufferedReader r = new BufferedReader(new InputStreamReader(in, cs));
        return r.lines().collect(Collectors.joining("\n"));
    }
}

Siccome un InputStream permette di leggere solamente la cruda sequenza di byte, per poter interpretare i byte in caratteri e poterli leggere agevolmente, abbiamo dovuto creare su di esso ben due altri flussi. Prima un InputStreamReader e poi un BufferedReader.

Adesso possiamo definire un metodo (sempre nella classe mp.web.Utils) che ci permette di leggere una pagina dal Web.

/** Ritorna una stringa con il contenuto della pagina localizzata dall'URL
 * dato usando la codifica per i caratteri specificata.
 * @param url  una stringa contenente un URL
 * @param cs  la codifica per i caratteri della pagina
 * @return  il contenuto della pagina come stringa
 * @throws IOException se accade un errore durante la connessione remota */
public static String loadPage(String url, Charset cs) throws IOException {
    URL urlO = new URL(url);
    URLConnection urlC = urlO.openConnection();
    urlC.setRequestProperty("User-Agent", "Mozilla/5.0");
    urlC.setRequestProperty("Accept", "text/html;q=1.0,*;q=0");
    urlC.setRequestProperty("Accept-Encoding", "identity;q=1.0,*;q=0");
    urlC.setConnectTimeout(5000);
    urlC.setReadTimeout(10000);
    urlC.connect();
    return read(urlC.getInputStream(), cs);
}

Si noti che nelle proprietà della richiesta di connessione abbiamo dichiarato di voler una risorsa di tipo text/html e che non vogliamo che sia compressa (identity). Come si sa, il server non è in alcun modo obbligato a rispettare le nostre richieste, ma tipicamente un server è "ben educato" e le rispetta nei limiti delle sue possibilità.

Per mettere alla prova i metodi appena definiti e altri che definiremo, creiamo una classe TestWeb sempre nel package mp.web.

/** Classe per testare operazioni relative al web */
public class TestWeb {
    public static void test_loadPage() {
        try {
            String page = Utils.loadPage("http://docs.oracle.com/javase/8/",
                    StandardCharsets.UTF_8);
            out.println("Page length: "+page.length());
        } catch (IOException e) { e.printStackTrace(); }
    }

    public static void main(String[] args) {
        test_loadPage();
    }
}

Ora che sappiamo come scaricare una pagina, vediamo come possiamo estrarre da essa delle informazioni che ci interessano.

Espressioni regolari per estrarre informazioni

Per estrarre dati o informazioni da una pagina potremmo fare un parsing HTML della pagina e scandagliare l'albero di parsing per ottenere le informazioni. Purtroppo in Java non è così facile fare il parsing dell'HTML a differenza di altri linguaggi (ad es. Python). Invero la libreria di Java mette a disposizione strumenti potenti per effettuare parsing HTML molto accurati, ma l'uso di tali strumenti è piuttosto laborioso. Non essendo le estrazioni che vogliamo fare molto complesse, preferiamo usare le espressioni regolari che la libreria di Java mette a disposizione nel package java.util.regex.

Il tipo di ricerche (o interrogazioni) che faremo sono piuttosto semplici. Consideriamo un sito, ad esempio, di un quotidiano, in cui è possibile fare una ricerca. Ad esempio, volendo fare una interrogazione relativa a "Carlo Padoan", il sito potrebbe servirsi di un URL del tipo,

"http://quotidiano.it/ricerca?query=Carlo+Padoan&sortby=date"

La risposta potrebbe essere una pagina in HTML al cui interno c'è l'informazione che ci interessa. Ad esempio il titolo delle notizia più recente che riguarda la nostra interrogazione.

Nella figura abbiamo evidenziato con sfondo rosso le parti che ci interessano, cioè il titolo e la data. Dopo aver esaminato con attenzione la struttura dell'HTML intorno alle parti che vogliamo estrarre, possiamo scrivere un'espressione regolare che le cattura entrambe.

Nella figura, per facilitare la comprensione, l'espressione regolare è stata suddivisa in sotto-espressioni, ognuna è evidenziata con un colore di sfondo uguale a quello della parte del testo che è riconosciuta da quella sotto-espressione. Ad esempio, la prima sotto-espressione <h1>[^<]* riconosce un qualsiasi testo che inizia con <h1> e prosegue con una sequenza di caratteri di lunghezza massimale che non contiene il carattere <. Infatti [^<] segnica un qualsiasi carattere eccetto < e * è un operatore che significa zero o più occorrenze di ciò che lo precede. Un gruppo è una sotto-espressione tra parentesi tonde.

Un oggetto di tipo Pattern, in java.util.regex, rappresenta un'espressione regolare (compilata). Può essere creato tramite il metodo static Pattern compile(String regex). Una volta ottenuto l'oggetto Pattern per applicarlo ad una stringa s occorre creare un oggetto Matcher tramite Matcher matcher(CharSequence s). Un oggetto Matcher permette di effettuare varie operazioni di riconoscimento di un'espressione regolare relativamente a una stringa. La principale è boolean find() che trova la prossima sotto-stringa che è riconosciuta dall'espressione regolare, se la trova ritorna true. Quindi la prima volta che il metodo find è invocato trova, se esiste, la prima sotto-stringa riconosciuta dall'espressione. Dopo un'invocazione di find che ritorna true, il metodo String group(int g) ritorna la sotto-stringa catturata dal gruppo g nella sotto-stringa riconosciuta da find.

Adesso possiamo definire una classe, che chiamiamo TheLatest (cioè "L'ultima") per rappresentare una sorgente web che può essere interrogata per notizie aggiornate, come ad esempio il sito di un quotidiano.

package mp.web;

/** Un oggetto {@code TheLatest} rappresenta un servizio web per la ricerca e il
 * recupero di informazioni aggiornate. In altri termini, un oggetto
 * {@code TheLatest} può essere interrogato circa un argomento specificato in
 * una stringa (vedi il metodo {@link mp.web.TheLatest#get(String) get()}). Ad
 * esempio, un oggetto {@code TheLatest} potrebbe rappresentare il servizio di
 * ricerca offerto da un quotidiano. L'attuale implementazione è infatti basata
 * proprio su quest'ulitmo tipo di interrogazioni (si veda il costruttore
 * {@link mp.web.TheLatest#TheLatest(String,java.nio.charset.Charset,String,
 String,String,int,int) TheLatest(...)}). */
public class TheLatest {
    /** Crea un oggetto per effettuare interrogazioni ad un servizio web tale
     * che la specifica dell'URL della pagina di risposta ad una interrogazione
     * {@code q} si ottiene con la concatenazione {@code uS + q + uE}. Inoltre
     * il titolo e la data della più recente notizia si può estrarre dalla
     * pagina di risposta tramite l'espressione regolare {@code re} e il numero
     * del gruppo che cattura il titolo è {@code gT} mentre quello per la data
     * è {@code gD}.
     * @param n  nome del servizio web
     * @param cs  codifica dei caratteri della pagina di risposta
     * @param uS  parte iniziale dell'URL di interrogazione
     * @param uE  parte finale dell'URL di interrogazione
     * @param re  espressione regolare per estrarre titolo e data
     * @param gT  numero del gruppo, in {@code re}, che cattura il titolo
     * @param gD  numero del gruppo, in {@code re}, che cattura la data */
    public TheLatest(String n, Charset cs, String uS, String uE, String re,
                     int gT, int gD) {
        name = n;
        charset = cs;
        urlStart = uS;
        urlEnd = uE;
        regExp = Pattern.compile(re);
        gTitle = gT;
        gDate = gD;
    }

    /** Ritorna la risposta ad una interrogazione a questo servizio web.
     * @param q  una stringa che contiene l'interrogazione
     * @return la risposta all'interrogazione o null se accade un errore */
    public String get(String q) {
        q = q.replace(" ", "+");
        try {
            String page = Utils.loadPage(urlStart + q + urlEnd, charset);
            Matcher m = regExp.matcher(page);
            String s = "";
            if (m.find()) {
                s += name+": "+m.group(gDate);
                s += " <<"+Utils.clean(m.group(gTitle))+">>";
            }
            return s;
        } catch (IOException e) { return null; }
    }

    private final String name;
    private final Charset charset;
    private final String urlStart, urlEnd;
    private final Pattern regExp;
    private final int gTitle, gDate;
}

Nell'implementazione del metodo get abbiamo usato il metodo clean che "ripulisce" una stringa. Siccome potrebbe risultare utile anche in altre occasioni, lo aggiungiamo alla classe mp.web.Utils.

/** Ritorna la stringa normalizzata (sostituzione di tutti i whitespaces con
 * lo spazio e riduzione di due o più spazi consecutivi ad uno solo) e con
 * sostituzione delle più comuni HTML character references.
 * @param s  una stringa
 * @return la stringa normalizzata e con sostituzione delle più comuni
 * HTML character references */
public static String clean(String s) {
    s = s.replaceAll("\\s+", " ");
    for (String[] cr : CHAR_REFS)
        s = s.replace("&"+cr[0]+";", cr[1]);
    return s;
}

/** Sostituzioni per le più comuni HTML character references */
private static final String[][] CHAR_REFS = {{"amp","&"},{"laquo","\""},
        {"raquo","\""},{"quot","\""},{"egrave","è"},{"Egrave","È"},
        {"eacute","é"},{"agrave","à"},{"ograve","ò"},{"igrave","ì"},
        {"ugrave","ù"},{"deg","°"}};

Spesso si vogliono fare molte interrogazioni a diversi servizi web. Conviene quindi definire un metodo che ci permetta di fare tutte queste interrogazioni. Consideriamo prima di tutto una semplice implementazione sequenziale di un tale metodo.

Interrogazioni sequenziali

Dati dei servizi web (cioè degli oggetti TheLatest) e delle interrogazioni, vogliamo interrogare ogni servizio web rispetto a tutte le interrogazioni. In questa prima implementazione le interrogazioni sono eseguite in modo sequenziale, cioè una dopo l'altra in un solo thread. Aggiungiamo quindi il seguente metodo statico alla classe mp.web.TheLatest.

/** Ritorna una mappa che associa ad ogni data interrogazione la lista delle
 * risposte ottenute dai servizi web specificati.
 * @param lts  i servizi web da interrogare
 * @param qq  le interrogazioni
 * @return  una mappa con le con risposte alle interrogazioni */
public static Map<String, List<String>> get(TheLatest[] lts, String...qq) {
    Map<String, List<String>> results = new HashMap<>();
    for (String q : qq) {
        List<String> list = new ArrayList<>();
        for (TheLatest lt : lts)
            list.add(lt.get(q));
        results.put(q, list);
    }
    return results;
}

A questo punto definiamo un metodo per mettere alla prova il metodo appena definito. Scriviamo il seguente metodo statico nella classe mp.web.TestWeb.

public static void test_TheLatest(TheLatest[] lts, String[] qq,
              BiFunction<TheLatest[],String[],Map<String,List<String>>> get) {
    out.println("servizi: "+lts.length+" interrogazioni: "+qq.length);
    long time = System.currentTimeMillis();
    Map<String, List<String>> results = get.apply(lts, qq);
    out.println(String.format("Tempo: %.2f secondi",
            (System.currentTimeMillis() - time)/1000.0));
    results.forEach((k, l) -> {
        out.println(k);
        l.forEach(out::println);
    });
}

Si noti che è stato implementato in una forma molto generale così che possa essere usato per mettere alla prova anche altre implementazioni del metodo che interroga i servizi web.

Adesso un po' di dati per fare il test. Nel metodo main di mp.web.TestWeb.

// I servizi web da interrogare
TheLatest[] lts = {new TheLatest("Repubblica", StandardCharsets.UTF_8,
        "http://ricerca.repubblica.it/ricerca/repubblica?query=",
        "&sortby=ddate&mode=phrase",
        "<h1>[^<]*<[^>]*>([^<]*)<[^<]*</h1>([^<]*<[^t][^<]*)*<time[^>]*>([^<]*)</time>", 1, 3),
        new TheLatest("Corriere", StandardCharsets.ISO_8859_1,
                "http://sitesearch.corriere.it/forward.jsp?q=", "#",
                "<span class=\"hour\">([^<]*)</span>([^<]*<[^h][^<]*)*<h1>[^<]*<[^>]*>([^<]*)<[^<]*</h1>", 3, 1),
        new TheLatest("Il Sole 24ore", StandardCharsets.ISO_8859_1,
                "http://www.ricerca24.ilsole24ore.com/fc?cmd=static&chId=30&path=%2Fsearch%2Fsearch_engine.jsp&keyWords=%22",
                "%22&orderByString=Data+desc",
                "<a[^>]*>([^<]*)</a></div></div><div class=\"box_autore\"><div class=\"autore_text\">[^<0-9]*([^<]*)</div>", 1, 2)};
// Le interrogazioni
String[] qq = {"Carlo Padoan","Matteo Renzi","Sofia Loren","Totti",
        "Belen","Barack Obama","Informatica","Nanni Moretti",
        "Federica Pellegrini","Beppe Grillo","spread",
        "debito pubblico","Università"};

test_TheLatest(lts, qq, TheLatest::get);

Provandolo potremmo ottenere qualcosa del tipo

servizi: 3 interrogazioni: 13
Tempo: 31.02 secondi
Università
Repubblica  23 aprile 2015  <<Fegato, i nuovi cocktail di farmaci guariscono il 90% dei malati>>
Corriere  16 April 2015  <<Renzi: Italia bella addormentata "Sulle riforme non si torna indietro">>
Il Sole 24ore  23/04/2015  <<Basket: addio a Lauren Hill, commosse il mondo>>
Matteo Renzi
Repubblica  23 aprile 2015  <<Festival dell'Economia di Trento: Stiglitz e Krugman parlano di Piketty>>
Corriere  23 April 2015  <<La spinta al bipartitismo che nell'Italicum non c'è>>
Il Sole 24ore  23/04/2015  <<Il divorzio breve è legge: sei mesi per dirsi addio>>
Barack Obama
. . .

Passiamo ora a un'implementazione parallela.

Computazioni asincrone

Per eseguire le interrogazioni in parallelo potremmo creare un certo numero di thread ed assegnare ad ognuno un'interrogazione. Però per casi come questo in cui bisogna eseguire molte computazioni o task (senza dipendenze tra loro) la libreria di Java offre alcuni utili strumenti. Per poterli usare dobbiamo prendere dimestichezza con alcuni interfacce. Prima di tutto l'interfaccia funzionale Callable<V>, in java.util.concurrent, che può rappresentare un task che a differenza di Runnable ritorna un valore. L'unico metodo dell'interfaccia è il seguente

V call() throws Exception
Calcola e ritorna un risultato di tipo V oppure fallisce e lancia un'eccezione.
Per i nostri scopi un Callable<String> ci permette di rappresentare il task che scarica una pagina e ne ritorna il contenuto in una stringa.

Lo strumento che gestisce l'esecuzione di molti task è rappresentato dall'interfaccia ExecutorService in java.util.concurrent. Un ExecutorService, o esecutore, usa internamente un gruppo (un pool) di thread che sono usati per eseguire i task che sono sottomessi all'esecutore. I metodi principali sono i seguenti.

<T> Future<T> submit(Callable<T> task)
Sottomette il task per essere eseguito e ritorna un Future che rappresenta l'esecuzione del task.
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException
Esegue tutti i tasks e ritorna la lista di Future che rappresentano i task eseguiti. Lancia InterruptedException se l'invocazione è interrotta (cioè il thread in cui è invocato il metodo è interrotto) in tal caso gli eventuali task non terminati sono cancellati.
void shutdown()
Inizia la chiusura dell'esecutore in cui gli eventuali task già sottomessi sono eseguiti, nessun altro task è accettato e quando tutti i task sono terminati anche tutti i thread sono terminati. Questo metodo dovrebbe sempre essere invocato quando l'esecutore non serve più perché molti tipi di esecutori mantengono in vita i thread che usano anche se non hanno task da eseguire.
Vediamo ora l'interfaccia Future<V> che rappresenta l'esecuzione e il risultato di una computazione asincrona, cioè un task eseguito su un qualche thread. La principale ragion d'essere di questa interfaccia sta proprio nel rappresentare task eseguiti da un ExecutorService. Infatti, gli ExecutorService disaccoppiano i task dai thread e quindi se si vuole cancellare un task eseguito in un ExecutorService non si può usare il metodo interrupted perché non si conosce il thread che sta eseguendo il task. Per questo l'interfaccia Future ha il metodo boolean cancel(boolean mayInterruptIfRunning) che imposta un flag interno di cancellazione. Il flag di cancellazione può essere letto con il metodo boolean isCancelled(). Gli altri metodi dell'interfaccia Future sono i seguenti.
V get() throws InterruptedException, ExecutionException
Aspetta, se necessario, che la computazione termini e ritorna il risultato. Lancia CancellationException se la computazione è cancellata. Lancia ExecutionException se la computazione lancia un'eccezione. Lancia InterruptedException se il thread corrente è interrotto mentre è in attesa.
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
Aspetta, se necessario per al più il tempo specificato, che la computazione termini e ritorna il risultato se disponibile. Lancia CancellationException se la computazione è cancellata. Lancia ExecutionException se la computazione lancia un'eccezione. Lancia InterruptedException se il thread corrente è interrotto mentre è in attesa. Lancia TimeoutException se il tempo d'attesa è stato superato.
boolean isDone()
Ritorna true se la computazione è completata tramite terminazione normale o un'eccezione o cancellazione.
Gli ExecutorService possono essere ottenuti dalla classe d'utilità Executors. Ci sono molti tipi diversi di esecutori che hanno caratteristiche che li rendono più o meno adatti in situazioni differenti. Nel nostro caso un buon tipo di esecutore è
ExecutorService newFixedThreadPool(int nThreads)
Crea un esecutore che usa un pool di thread con esattamente nThreads thread. I task sottomessi sono eseguiti dal fissato pool di thread che rimangono in vita fino alla chiusura dell'esecutore.

Interrogazioni parallele

Adesso possiamo implementare la versione parallela del metodo che effettua le interrogazioni.

/** Ritorna una mappa che associa ad ogni data interrogazione la lista delle
 * risposte ottenute dai servizi web specificati. L'implementazione usa il
 * numero di thread specificato.
 * @param nt  numero thread
 * @param lts  i servizi web da interrogare
 * @param qq  le interrogazioni
 * @return  una mappa con le risposte alle interrogazioni */
public static Map<String, List<String>> getParallel(int nt, TheLatest[] lts,
                                                    String...qq) {
    List<Callable<String>> tasks = new ArrayList<>();
    for (String q : qq)
        for (TheLatest lt : lts)
            tasks.add(() -> q+":"+lt.get(q));
    ExecutorService exec = Executors.newFixedThreadPool(nt);
    Map<String, List<String>> results = new HashMap<>();
    try {
        List<Future<String>> res = exec.invokeAll(tasks);
        for (Future<String> r : res) {
            String s = r.get();
            int i = s.indexOf(":");
            String q = s.substring(0, i), d = s.substring(i + 1);
            results.merge(q, Arrays.asList(d), (l1, l2) -> {
                List<String> l = new ArrayList<>(l1);
                l.addAll(l2);
                return l;
            });
        }
    } catch (InterruptedException | ExecutionException e) {}
    exec.shutdown();
    return results;
}

Provandolo potremmo ottenere

servizi: 3 interrogazioni: 13
Tempo: 5.75 secondi
Università
Repubblica  23 aprile 2015  <<Fegato, i nuovi cocktail di farmaci guariscono il 90% dei malati>>
Corriere  16 April 2015  <<Renzi: Italia bella addormentata "Sulle riforme non si torna indietro">>
Il Sole 24ore  23/04/2015  <<Basket: addio a Lauren Hill, commosse il mondo>>
. . .

Al di là dei particolari tempi osservati, che possono cambiare dipendendo da molti fattori (traffico della rete, carico dei server, ecc.), sicuramente il tempo d'esecuzione della versione parallela sarà molto più breve di quello della versione sequenziale. La ragione è che in questo caso i task effettuano un accesso a un server remoto e questo richiede un tempo significativo d'attesa durante il quale il task non fa nulla, cioè uno spreco di tempo. Se però questi accessi sono eseguiti in parallelo durante il tempo d'attesa di un task si può eseguire un altro task. In questi casi conviene quasi sempre usare un gran numero di thread paralleli, spesso un numero pari al numero di task, proprio per cercare di "riempire" il più possibile i tempi d'attesa.

Esercizi

[NumeroThread]    Provare il metodo che effettua le interrogazioni variando il numero di thread. Inoltre provare ad usare altri esecutori, ad esempio newCachedThreadPool. Che differenze si osservano?

[TheLatest+]    Aggiungere un altro servizio web ai tre già usati. Si provi con qualche altro quotidiano. Si va sul sito, si fa una ricerca e si esamina l'URL della pagina di risposta alla ricerca effettuata...

[Pagine]    Scrivere un programma che chiede all'utente un URL e sottomette il task per scaricare la pagina ad un esecutore e poi rimane in attesa che l'utente immetta un altro URL e così via. Ogni volta che viene immesso un nuovo URL il programma sottomette all'esecutore un task per scaricare la pagina. Il task che scarica la pagina dovrebbe ritornare non solo una stringa con il contenuto della pagina ma anche una stringa contenente la rappresentazione della mappa dei campi dell'header ritornato dal server che si può ottenere tramite il metodo getHeaderFields di URLConnection. Ovviamente ogni volta che una pagina è scaricata i dati della pagina sono stampati.

[Links]    Scrivere un programma che chiede all'utente un URL e sottomette il task per scaricare la pagina ad un esecutore. Non appena la pagina è scaricata, trova tutti i link (a pagine HTML) contenuti e sottomette all'esecutore i task per scaricare le relative pagine.

[Crawler]    Implementare un Web crawler. Dall'URL di una pagina iniziale si trovano i link contenuti in essa e si scaricano le pagine relative ai link poi si ripete l'operazione per ognuna delle pagine scaricate e così via. I link possono essere trovati da un'opportuna espressione regolare e dovrebbero essere seguiti solo se sono link a pagine HTML. Volendo si può cercare di implementare in Java il crawler dell'ultima lezione di Fondamenti di Programmazione.

24 Apr 2015