Metodologie di Programmazione: Lezione 14

Riccardo Silvestri

Web e computazioni asincrone

Scaricare risorse dal Web (pagine, immagini, ecc.) è uno dei compiti tipici in cui l'esecuzione multithreading porta a miglioramenti notevoli delle prestazioni. La ragione è molto semplice, ogni task, cioè scaricare una singola risorsa, è indipendente dagli altri task, la sua esecuzione consiste prevalentemente nell'attesa che il server remoto risponda e le attese possono essere "eseguite" in parallelo persino da un singolo processore e in parallelo con una vera esecuzione che usa la CPU. In altri termini mentre un task è in attesa non consuma tempo di CPU (o ne consuma pochissimo) e quindi tale tempo può essere usato per eseguire una vera computazione e anche per altre attese.

Un task che generalmente calcola un risultato ed è 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. Considereremo i servizi di ricerca offerti da alcuni quotidiani. Ad esempio per avere le ultime notizie sull'argomento Università si può digitare tale termine nella pagina Corriere ottenendo gli articoli più recenti del Corriere della Sera sull'argomento. Per automatizzare l'interrogazione, la digitazione manuale del termine è sostituita con la costruzione di un opportuno indirizzo web e il downloading della relativa pagina. Per prima cosa vedremo quindi come usare gli indirizzi web (cioè gli URL) e come scaricare le relative pagine tramite la libreria di Java. Inoltre siccome la pagina, risultato della ricerca, contiene anche molte cose non ci interessano, vedremo come usare le espressioni regolari (sempre fornite dalla libreria Java) per estrarre dalla pagina i titoli e le date degli articoli. Quindi implementeremo una prima versione sequenziale della nostra applicazione in cui le interrogazioni relative a vari termini e a vari quotidiani sono eseguite in modo appunto sequenziale. Infine sfruttando gli strumenti che Java offre per le computazioni asincrone implementeremo la versione multithreading.

Web & URL

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 gli indirizzi web, cioè gli URL, e le connessioni a server remoti per scaricare pagine.

Un oggetto della 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 HTML ma può anche essere un immagine, un video, un PDF, ecc. Un 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, si potrebbe pensare di usare uno dei due metodi seguenti per scaricare la risorsa: Object getContent() throws IOException o InputStream openStream() throws IOException. Ma questi 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 è aperta quando il metodo è invocato ma solo quando è invocato un metodo di URLConnection che richiede esplicitamente che la connessione sia aperta.

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 è importante impostare anche i tempi massimi d'attesa (timeout) della connessione. I timeout di default sono impostati a 0 che significa che non c'è alcun timeout. Il metodo void setConnectTimeout(int timeout) imposta il timeout per aprire la connessione e void setReadTimeout(int timeout) imposta quello per la lettura. In entrambi i casi il timeout è espresso in millisecondi.

Dopo 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 i seguenti due metodi.

void connect() throws IOException
Apre una connessione relativamente all'URL di questa URLConnection. Lancia una SocketTimeoutException se la connessione non è aperta entro il timeout impostato. Lancia IOException se accade un errore di I/O durante l'apertura.
InputStream getInputStream() throws IOException
Ritorna un flusso di input per leggere dalla connessione aperta. Il flusso ritornato lancia SocketTimeoutException se una richiesta di lettura non è esaudita entro il timeout di lettura 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.

Per scrivere un metodo che legge una pagina HTML dal Web ci manca solamente 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. 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 decodificando la sequenza di bytes di un
     * flusso tramite una codifica specificata. I fine linea sono sostituiti
     * con '\n'.
     * @param in  un flusso di bytes
     * @param cs  codifica per i caratteri
     * @return una stringa con i caratteri decodificati dal flusso */
    public static String read(InputStream in, Charset cs) {
        BufferedReader reader = new BufferedReader(new InputStreamReader(in, cs));
        return reader.lines().collect(Collectors.joining("\n"));
    }
}

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

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

/** Ritorna una stringa con il contenuto della pagina localizzata da un URL
 * usando una codifica per i caratteri specificata.
 * @param url  una stringa contenente un URL
 * @param cs  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 {
    URLConnection urlC = new URL(url).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 abbiamo dichiarato di volere una risorsa di tipo text/html e di non volere che sia compressa (identity). Come si sa, il server non è 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 main(String[] args) {
        test_loadPage();
    }

    private 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(); }
    }
}

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

Espressioni regolari per estrarre informazioni

Potremmo fare il parsing 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). La libreria di Java mette a disposizione strumenti potenti per effettuare parsing HTML, ma funzionano solamente per pagine scritte in un HTML perfetto che non è quasi mai praticato nel wild Web. Ad ogni modo per le estrazioni che ci servono le espressioni regolari, offerte nel package java.util.regex, sono sufficienti.

Il tipo di ricerche (o interrogazioni) che faremo sono piuttosto semplici. Consideriamo il sito di un ipotetico quotidiano, in cui è possibile fare ricerche. Ad esempio, per fare un'interrogazione su "Carlo Padoan", il sito potrebbe usare un URL del tipo,

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

La risposta sarà una pagina HTML che contiene le informazioni che ci interessano.

Nella figura sono evidenziate su sfondo rosso le parti che ci interessano, cioè il titolo della notizia 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 del testo che è catturato 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 Pattern1, in java.util.regex, rappresenta un'espressione regolare (compilata). Può essere creato tramite il metodo statico 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 Matcher permette di effettuare varie operazioni, la principale è boolean find() che trova la prossima sotto-stringa catturata dall'espressione regolare, se la trova ritorna true. Quindi la prima volta che è invocato trova, se esiste, la prima sotto-stringa catturata 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 catturata da find().

Definiamo una classe TheLatest (cioè L'ultima) per rappresentare una sorgente web che può essere interrogata per notizie aggiornate. Cerchiamo di definirla nel modo più generale possibile così da poter essere usata per le ricerche su diversi quotidiani. Per un particolare servizio web bisogna specificare come si costruisce l'URL dell'interrogazione e come si estraggono le informazioni che interessano. Generalmente l'URL dell'interrogazione ha il seguente formato:

<URL-start><query><URL-end>

dove <URL-start> e <URL-end> sono parti fisse dell'URL e <query> è il testo dell'interrogazione. Ad esempio per l'URL di interrogazione

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

si ha che <URL-start> = http://quotidiano.it/ricerca?query=, <query> = Carlo+Padoan e <URL-end> = &sortby=date. Quindi per un particolare servizio basterà specificare le parti <URL-start> e <URL-end>. Per l'estrazione delle informazioni si può specificare un'espressione regolare e i relativi numeri dei gruppi che catturano le informazioni d'interesse.

package web;

import java.nio.charset.Charset;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Un oggetto {@code TheLatest} rappresenta un servizio web per la ricerca e il
 * recupero di informazioni aggiornate. Può essere interrogato circa un argomento
 * specificato in una stringa (vedi {@link TheLatest#get(String)}). Ad esempio,
 * un {@code TheLatest} può rappresentare il servizio di ricerca offerto da un
 * quotidiano. L'attuale implementazione è infatti basata proprio su quest'ultimo
 * tipo di interrogazioni (si veda il costruttore
 * {@link TheLatest#TheLatest(String,Charset,String,String,String,int,int)}). */
public class TheLatest {
    /** Crea un TheLatest tale che l'URL di un'interrogazione {@code q} è la
     * concatenazione {@code uS + q + uE} e il titolo e data della più recente
     * notizia si può estrarre dalla pagina di risposta tramite l'espressione
     * regolare {@code re} dove il numero del gruppo che cattura il titolo è
     * {@code gT} mentre quello per la data è {@code gD}.
     * @param nm  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 nm, Charset cs, String uS, String uE, String re,
                     int gT, int gD) {
        name = nm;
        charset = cs;
        urlStart = uS;
        urlEnd = uE;
        regExp = Pattern.compile(re);
        gTitle = gT;
        gDate = gD;
    }

   /** Ritorna la risposta ad un'interrogazione a questo servizio web.
     * @param q  stringa che contiene l'interrogazione
     * @return  la risposta all'interrogazione */
    public String get(String q) {
        String url = urlStart+q.replace(" ", "+")+urlEnd;
        String s = name+"  ";
        try {
            String page = Utils.loadPage(url, charset);
            Matcher m = regExp.matcher(page);
            if (m.find()) {
                s += m.group(gDate).trim()+"  ";
                return s + "<<"+Utils.clean(m.group(gTitle).trim())+">>";
            } else
                return s + "No news";
        } catch (Exception e) { return s+"ERROR "+e.getMessage(); }
    }

    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 tratta da una pagina HTML di modo che possa essere stampata in plain text. Siccome potrebbe risultare utile anche in altre occasioni, lo aggiungiamo alla classe mp.web.Utils.

/** Ritorna la stringa ripulita, cioè ottenuta sostituendo le sequenze di
 * whitespaces consecutivi con un singolo spazio e le più comuni HTML
 * character references con i relativi caratteri.
 * @param s  una stringa, tipicamente tratta da una pagina HTML
 * @return la stringa ripulita */
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","°"}};

Generalmente si vogliono fare parecchie interrogazioni a vari servizi web. Conviene quindi definire un metodo che ci permetta di fare le interrogazioni e che ne ritorna i risultati.

Interrogazioni sequenziali

Dati alcuni servizi web, cioè oggetti TheLatest, e alcune interrogazioni, vogliamo sottoporre le interrogazioni ad ogni servizio web. In questa prima implementazione le interrogazioni sono eseguite in modo sequenziale, cioè una dopo l'altra in un singolo thread. Aggiungiamo quindi il seguente metodo statico alla classe mp.web.TheLatest.

/** Ritorna una mappa che associa ad ogni interrogazione data la lista delle
 * risposte ottenute dai servizi web specificati. L'implementazione è
 * sequenziale nel thread di invocazione.
 * @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>> get(TheLatest[] lts, String...qq) {
    Map<String,List<String>> res = new HashMap<>();
    for (String q : qq) {
        List<String> r = new ArrayList<>();
        for (TheLatest lt : lts)
            r.add(lt.get(q));
        res.put(q, r);
    }
    return res;
}

Definiamo un metodo in mp.web.TestWeb per mettere alla prova il metodo appena definito,

private 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);
    });
}

È stato definito in modo generale così che possa essere usato per mettere alla prova anche altre implementazioni del metodo che interroga i servizi web come quella multithreading che vedremo fra poco. 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\">\\D*([^<]*)</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","Donald Trump",
        "Federica Pellegrini","Beppe Grillo","emigranti",
        "debito pubblico","Università","Trivelle","Referendum"};

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

Provandolo potremmo ottenere qualcosa come

Servizi: 3  Interrogazioni: 15
Tempo: 48.49 secondi
Matteo Renzi
Repubblica  20 aprile 2016  <<I banchieri iraniani a caccia di affari in Italia>>
Corriere  20 April 2016  <<Indagato l'assessore al Bilancio di Livorno, Raggi, "io non mi esprimo">>
Il Sole 24ore  20/04/2016  <<Decreto banche rinviato alla prossima settimana>>
Barack Obama
Repubblica  20 aprile 2016  <<L'Arabia Saudita lancia il suo primo bond. Obama è a Riad>>
Corriere  19 April 2016  <<Quei dirigenti di Palazzo Chigi ancora tutti virtuosi (e premiati)>>
Il Sole 24ore  20/04/2016  <<Gli amici della Gran Bretagna hanno ragione ad aver paura di una Brexit>>
Totti
. . .

Passiamo ora all'implementazione multithreading, prima però dobbiamo considerare alcuni strumenti offerti dalla libreria di Java.

Computazioni asincrone

Per eseguire le interrogazioni potremmo creare un certo numero di thread ed assegnare ad ognuno un'interrogazione da eseguire. Però per casi come questo che consistono nell'esecuzione di task indipendenti, cioè senza dipendenze tra loro, la libreria Java offre alcuni utili strumenti. Prima di usarli dobbiamo prendere dimestichezza con alcune interfacce. L'interfaccia funzionale Callable<V>, in java.util.concurrent, rappresenta 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> rappresenta un task che effettua un'interrogazione e ne ritorna il risultato.

Un esecutore di task è rappresentato dall'interfaccia ExecutorService in java.util.concurrent. Le implementazioni di ExecutorService gestiscono internamente un insieme (un pool) di thread per eseguire i task che sono sottomessi all'esecutore. Lo scopo principale di tale interfaccia è di disaccoppiare i task da eseguire dai thread usati per eseguirli. In questo modo un thread può essere riusato più facilmente per eseguire più task, permettendo così una gestione efficiente dei thread. 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. Il metodo ritorna immediatamente e il risultato del task può essere ottenuto tramite i metodi di Future che saranno spiegati fra poco.
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException
Esegue tutti i tasks, aspetta che terminano tutti e ritorna la lista di Future che rappresentano i task eseguiti. Lancia InterruptedException se l'invocazione è interrotta durante l'attesa (cioè il thread in cui è invocato il metodo è interrotto), in tal caso gli eventuali task non ancora terminati sono cancellati.
void shutdown()
Inizia la chiusura dell'esecutore in cui i task già sottomessi sono portati a termine, nessun altro task è accettato (cioè, il metodo submit lancia RejectedExecutionException) e quando tutti i task sono terminati anche tutti i thread interni sono terminati. Si dovrebbe sempre invocare questo metodo quando l'esecutore non serve più perché molti esecutori mantengono in vita i thread interni anche se non hanno task da eseguire.
L'interfaccia Future<V> 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. Siccome i thread di un ExecutorService non sono accessibili pubblicamente (per far sì che l'esecutore abbia pieno controllo su di essi), se si vuole cancellare un task non si può usare il metodo interrupted perché non si conosce il thread che sta eseguendo il task. Perciò l'interfaccia Future ha il metodo boolean cancel(boolean mayInterruptIfRunning) che imposta un flag di cancellazione interno al Future. 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 il task termini e ritorna il risultato. Lancia CancellationException se il task è cancellato, cioè il task è stato cancellato prima che terminasse2. Lancia ExecutionException se il task lancia un'eccezione, cioè il task è terminato a causa dell'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 il task termini e ritorna il risultato se è disponibile. Lancia TimeoutException se il tempo d'attesa è stato superato (e il task non è né terminato né cancellato). Lancia CancellationException se il task è cancellato (o era già cancellato o è stato cancellato durante l'attesa). Lancia ExecutionException se il task lancia un'eccezione. Lancia InterruptedException se il thread corrente è interrotto mentre è in attesa.
boolean isDone()
Ritorna true se il task è completato o terminando normalmente o tramite un'eccezione o è stato cancellato.
Molti tipi di ExecutorService possono essere ottenuti tramite metodi statici (factory methods) dalla classe 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 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.
Altri tipi di esecutori saranno usati in lezioni successive.

Interrogazioni parallele

Implementiamo la versione multithreading del metodo che effettua le interrogazioni. Usiamo l'esecutore fornito da newFixedThreadPool con un numero di thread che è passato come parametro del metodo, così si può facilmente sperimentare variando il numero di thread. Ogni interrogazione sarà eseguita da un task distinto, definito da un Callable<String>. Come nell'implementazione sequenziale collezioneremo i risultati dei task delle interrogazioni in una mappa. Quindi dovremmo sottomettere ognuno dei task all'esecutore e poi chiederne il risultato con il metodo get() del Future<String> ritornato dal metodo submit. Però bisogna fare attenzione all'ordine con il quale sottomettiamo i task e ne chiediamo i risultati. Se ad esempio ogni task fosse sottomesso e subito dopo si chiedesse il risultato, cioè prima di aver sottomesso tutti i task, vanificheremmo i vantaggi dell'esecuzione multithreading. Perché in ogni momento avremmo un singolo task in esecuzione. Infatti l'invocazione del get() per ottenere il risultato aspetta che il task sia terminato e in quel lasso di tempo non possiamo sottomettere altri task perché il thread corrente è bloccato nell'attesa. Quindi l'unico modo che ci consente di sfruttare al meglio il multithreading è di sottomettere tutti i task e solamente dopo chiederne tutti i risultati. Così quando saremo in attesa per il risultato di un task ci saranno molti altri task in esecuzione nei thread gestiti dall'esecutore.

/** Ritorna una mappa che associa ad ogni data interrogazione la lista delle
 * risposte ottenute dai servizi web specificati. L'implementazione è
 * multithreading ed 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>> get(int nt, TheLatest[] lts, String...qq) {
    ExecutorService exec = Executors.newFixedThreadPool(nt);    // Esecutore
    Map<String,List<Future<String>>> futures = new HashMap<>();
    for (String q : qq) {   // Sottomette i task di tutte le interrogazioni
        List<Future<String>> list = new ArrayList<>();
        for (TheLatest lt : lts)     // Sottomette i task di una interrogazione
            list.add(exec.submit(() -> lt.get(q)));           // ai servizi web
        futures.put(q, list);
    }
    Map<String,List<String>> results = new HashMap<>();
    for (String q : qq) {  // Ottiene i risultati dei task delle interrogazioni
        List<String> list = new ArrayList<>();
        for (Future<String> f : futures.get(q))          // Ottiene i risultati
            try {              // dei task di una interrogazione ai servizi web
                list.add(f.get());
            } catch (InterruptedException | ExecutionException e) {
                list.add("ERROR: "+e.getMessage());
            }
        results.put(q, list);
    }
    exec.shutdown();
    return results;
}

Provandolo con 10 thread potremmo ottenere

Servizi: 3  Interrogazioni: 15
Tempo: 8.93 secondi
Università
Informatica
Repubblica  21 aprile 2016  <<Sos di prof e ricercatori “Tagli e poche iscrizioni l’università è in crisi”>>
Corriere  20 April 2016  <<La neo direttrice di Twitter in Cina vicina a Xi Jinping e la pubblicità>>
Il Sole 24ore  20/04/2016  <<Sfide (e alleanze) sugli smartwatch>>
. . .

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 multithreading sarà significativamente minore di quello della versione sequenziale. La ragione è che i task effettuano una connessione a un server remoto e questo comporta un tempo significativo d'attesa durante il quale il task non fa nulla, cioè uno spreco di tempo. Se però le connessioni sono eseguite in parallelo, durante il tempo d'attesa di un task possono essere eseguiti altri task. In questi casi conviene quasi sempre usare un gran numero di thread, anche un numero pari al numero di task, proprio per cercare di "riempire" il più possibile i tempi d'attesa. Il numero ottimale di thread per il downloading di risorse da server remoti, è difficile da determinare a priori perché dipende ma molti fattori tra cui la larghezza di banda della rete, il traffico attuale, le politiche adottate dai server, il numero di server coinvolti, ecc. Sicuramente un numero di thread superiore al numero di task non porta mai a miglioramenti ma probabilmente peggiora le prestazioni.

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?

[URLs]    Definire un metodo (statico) Map<String,Integer> pageLen(String...urls) che prese in input delle stringhe che contengono indirizzi a pagine web, scarica le pagine e ritorna una mappa con chiavi gli indirizzi e ad ognuno è associata la lunghezza della relativa pagina. Dare un'implementazione sequenziale e una multithreading. Per quest'ultima cercare di ottimizzare il numero di thread e/o l'esecutore anche in funzione del numero di indirizzi.

[FibFuture]    Scrivere un programma che chiede in continuazione all'utente di digitare un intero positivo n e in risposta inizia il calcolo dell'n-esimo numero di Fibonacci (si veda la lezione precedente). I calcoli dei numeri di Fibonacci sono eseguiti in multithreading sia per mantenere la reattività all'input dell'utente sia per eseguire più calcoli contemporaneamente. Non appena un calcolo di quelli iniziati termina, il programma stampa il risultato. Usare un opportuno esecutore, ad es. newCachedThreadPool che non pone limiti sul numero di thread. Il programma dovrebbe cercare di stampare il risultato relativo alla computazione che termina per prima tra quelle che sono state iniziate. Per fare ciò si può usare il metodo get(long timeout, TimeUnit unit) di Future con un certo timeout.

[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 semplice 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.

21 Apr 2016


  1. La documentazione della classe Pattern contiene anche una descrizione della sintassi delle espressioni regolari.

  2. Se il metodo cancel è invocato prima che il task termini (se è invocato dopo, non ha alcun effetto), una qualsiasi invocazione successiva dei metodi get lancerà CancellationException indipendentemente da come si comporterà il task riguardo alla cancellazione. Perciò dopo la cancellazione, anche se il task continua la sua computazione fino alla fine, il suo risultato non sarà accessibile con i metodi get. Quindi un task dovrebbe essere sensibile alla cancellazione invocando periodicamente il metodo isCancelled() per evitare di consumare inutilmente risorse di calcolo.