Executor e Thread Pool
Abbiamo già esplorato una varietà di meccanismi per supportare il multithreading, e Executors è uno di questi!
Cosa sono gli Executors e il Thread Pooling?
Executors è un meccanismo che offre astrazioni di alto livello per la gestione dei thread. Permette di creare e gestire un thread pool, che consiste in un insieme di thread preesistenti pronti a eseguire compiti. Invece di creare un nuovo thread per ogni compito, i compiti vengono inviati al pool, dove la loro esecuzione viene distribuita tra i thread.
Ma che cos'è esattamente un thread pool? È una raccolta di thread preesistenti pronti a eseguire compiti. Utilizzando un thread pool, si evita il sovraccarico di creare e distruggere thread ripetutamente, poiché gli stessi thread possono essere riutilizzati per più compiti.
Se ci sono più task che thread, i task attendono nella Task Queue. Un task dalla coda viene gestito da un thread disponibile dal pool e, una volta completato il task, il thread prende un nuovo task dalla coda. Una volta che tutti i task nella coda sono terminati, i thread rimangono attivi e attendono nuovi task.
Esempio dalla vita reale
Immagina un ristorante in cui i cuochi (thread) preparano ordini (task). Invece di assumere un nuovo cuoco per ogni ordine, il ristorante impiega un numero limitato di cuochi che gestiscono gli ordini man mano che arrivano. Una volta che un cuoco termina un ordine, prende il successivo, il che aiuta a utilizzare in modo efficiente le risorse del ristorante.
Metodo principale
newFixedThreadPool(int n): Crea un pool con un numero fisso di thread pari a n.
Main.java
1ExecutorService executorService = Executors.newFixedThreadPool(20);
newCachedThreadPool(): Crea un pool che può creare nuovi thread secondo le necessità, ma riutilizzerà i thread disponibili se presenti.
Main.java
1ExecutorService executorService = Executors.newCachedThreadPool();
newSingleThreadExecutor(): Crea un pool a thread singolo che garantisce che i task vengano eseguiti in modo sequenziale, ovvero uno dopo l'altro. Utile per task che devono essere eseguiti in ordine rigoroso.
Main.java
1ExecutorService executorService = Executors.newSingleThreadExecutor();
In tutti gli esempi, i metodi di Executors restituiscono un'implementazione dell'interfaccia ExecutorService, utilizzata per gestire i thread.
ExecutorService fornisce metodi per la gestione di un pool di thread. Ad esempio, submit(Runnable task) accetta un'attività come oggetto Runnable e la inserisce in una coda per l'esecuzione. Restituisce un oggetto Future, che può essere utilizzato per verificare lo stato dell'attività e ottenere un risultato se l'attività produce un risultato.
Main.java
12345678910111213141516171819202122232425262728293031323334package com.example; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { // Create a thread pool with 5 threads ExecutorService executor = Executors.newFixedThreadPool(5); // Define the task to be executed Runnable task = () -> { System.out.println("Task is running: " + Thread.currentThread().getName()); try { Thread.sleep(2000); // Simulate some work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Task completed: " + Thread.currentThread().getName()); }; // Submit the task for execution and get a `Future` Future<?> future = executor.submit(task); // Check if the task is done System.out.println("Is task done? " + future.isDone()); // You can use `future` to check the status of the task or wait for its completion // Example: future.get() - blocks until the task is completed (not used in this example) // Initiate an orderly shutdown of the executor service executor.shutdown(); } }
Il metodo shutdown() avvia un'interruzione graduale del pool di thread. Smette di accettare nuove attività ma completerà quelle attualmente in esecuzione. Una volta chiamato questo metodo, il pool non può essere riavviato.
Il metodo awaitTermination(long timeout, TimeUnit unit) attende che tutte le attività nel pool vengano completate entro il tempo specificato. Si tratta di un'attesa bloccante che consente di assicurarsi che tutte le attività siano completate prima di finalizzare il pool.
Inoltre, non abbiamo menzionato la principale interfaccia che aiuta a tracciare lo stato del thread, ovvero l'interfaccia Future. Il metodo submit() dell'interfaccia ExecutorService restituisce un'implementazione dell'interfaccia Future.
Se si desidera ottenere il risultato dell'esecuzione del thread, è possibile utilizzare il metodo get(). Se il thread implementa Runnable, il metodo get() non restituisce nulla, ma se implementa Callable<T>, restituisce un valore di tipo T.
Main.java
12345678910111213141516171819202122232425262728293031package com.example; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); // Callable task that returns a result Callable<String> task = () -> { Thread.sleep(1000); // Simulate some work return "Task result"; }; Future<String> future = executor.submit(task); try { // Get the result of the task String result = future.get(); System.out.println("Task completed with result: " + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } executor.shutdown(); } }
È inoltre possibile utilizzare il metodo cancel(boolean mayInterruptIfRunning) per tentare di annullare l'esecuzione di un task. Se il task non è ancora iniziato, verrà annullato. Se il task è già in esecuzione, potrebbe essere interrotto in base al flag mayInterruptIfRunning.
true: Se il task è in esecuzione, verrà interrotto chiamando Thread.interrupt() sul thread in esecuzione.
false: Se il task è in esecuzione, non verrà interrotto e il tentativo di annullamento non avrà effetto sul task attualmente in esecuzione.
E anche 2 metodi che intuitivamente si comprendono dal loro nome:
isCancelled(): Verifica se il task è stato annullato;isDone(): Verifica se il task è stato completato.
Esempio di utilizzo
La dimensione del thread pool dipende dalla natura dei task eseguiti. Generalmente, la dimensione del thread pool non dovrebbe essere codificata in modo statico; dovrebbe invece essere personalizzabile. La dimensione ottimale viene determinata monitorando il throughput dei task eseguiti.
È massimamente efficiente utilizzare un numero di threads = processor cores. Questo può essere verificato nel codice utilizzando Runtime.getRuntime().availableProcessors().
Main.java
1int availableProcessors = Runtime.getRuntime().availableProcessors();
Differenze tra la Creazione Diretta di Thread e l'utilizzo di ExecutorService
Le principali differenze tra la creazione diretta di thread e l'utilizzo di ExecutorService riguardano la comodità e la gestione delle risorse. La creazione manuale dei thread richiede la gestione individuale di ciascun thread, il che complica il codice e l'amministrazione.
ExecutorService semplifica la gestione utilizzando un thread pool, facilitando la gestione dei task. Inoltre, mentre la creazione manuale dei thread può portare a un elevato consumo di risorse, ExecutorService consente di personalizzare la dimensione del thread pool.
Grazie per i tuoi commenti!
Chieda ad AI
Chieda ad AI
Chieda pure quello che desidera o provi una delle domande suggerite per iniziare la nostra conversazione
Can you explain more about how ExecutorService manages threads?
What are some best practices for using thread pools?
How does the Future interface help in managing task results?
Awesome!
Completion rate improved to 3.33
Executor e Thread Pool
Scorri per mostrare il menu
Abbiamo già esplorato una varietà di meccanismi per supportare il multithreading, e Executors è uno di questi!
Cosa sono gli Executors e il Thread Pooling?
Executors è un meccanismo che offre astrazioni di alto livello per la gestione dei thread. Permette di creare e gestire un thread pool, che consiste in un insieme di thread preesistenti pronti a eseguire compiti. Invece di creare un nuovo thread per ogni compito, i compiti vengono inviati al pool, dove la loro esecuzione viene distribuita tra i thread.
Ma che cos'è esattamente un thread pool? È una raccolta di thread preesistenti pronti a eseguire compiti. Utilizzando un thread pool, si evita il sovraccarico di creare e distruggere thread ripetutamente, poiché gli stessi thread possono essere riutilizzati per più compiti.
Se ci sono più task che thread, i task attendono nella Task Queue. Un task dalla coda viene gestito da un thread disponibile dal pool e, una volta completato il task, il thread prende un nuovo task dalla coda. Una volta che tutti i task nella coda sono terminati, i thread rimangono attivi e attendono nuovi task.
Esempio dalla vita reale
Immagina un ristorante in cui i cuochi (thread) preparano ordini (task). Invece di assumere un nuovo cuoco per ogni ordine, il ristorante impiega un numero limitato di cuochi che gestiscono gli ordini man mano che arrivano. Una volta che un cuoco termina un ordine, prende il successivo, il che aiuta a utilizzare in modo efficiente le risorse del ristorante.
Metodo principale
newFixedThreadPool(int n): Crea un pool con un numero fisso di thread pari a n.
Main.java
1ExecutorService executorService = Executors.newFixedThreadPool(20);
newCachedThreadPool(): Crea un pool che può creare nuovi thread secondo le necessità, ma riutilizzerà i thread disponibili se presenti.
Main.java
1ExecutorService executorService = Executors.newCachedThreadPool();
newSingleThreadExecutor(): Crea un pool a thread singolo che garantisce che i task vengano eseguiti in modo sequenziale, ovvero uno dopo l'altro. Utile per task che devono essere eseguiti in ordine rigoroso.
Main.java
1ExecutorService executorService = Executors.newSingleThreadExecutor();
In tutti gli esempi, i metodi di Executors restituiscono un'implementazione dell'interfaccia ExecutorService, utilizzata per gestire i thread.
ExecutorService fornisce metodi per la gestione di un pool di thread. Ad esempio, submit(Runnable task) accetta un'attività come oggetto Runnable e la inserisce in una coda per l'esecuzione. Restituisce un oggetto Future, che può essere utilizzato per verificare lo stato dell'attività e ottenere un risultato se l'attività produce un risultato.
Main.java
12345678910111213141516171819202122232425262728293031323334package com.example; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { // Create a thread pool with 5 threads ExecutorService executor = Executors.newFixedThreadPool(5); // Define the task to be executed Runnable task = () -> { System.out.println("Task is running: " + Thread.currentThread().getName()); try { Thread.sleep(2000); // Simulate some work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Task completed: " + Thread.currentThread().getName()); }; // Submit the task for execution and get a `Future` Future<?> future = executor.submit(task); // Check if the task is done System.out.println("Is task done? " + future.isDone()); // You can use `future` to check the status of the task or wait for its completion // Example: future.get() - blocks until the task is completed (not used in this example) // Initiate an orderly shutdown of the executor service executor.shutdown(); } }
Il metodo shutdown() avvia un'interruzione graduale del pool di thread. Smette di accettare nuove attività ma completerà quelle attualmente in esecuzione. Una volta chiamato questo metodo, il pool non può essere riavviato.
Il metodo awaitTermination(long timeout, TimeUnit unit) attende che tutte le attività nel pool vengano completate entro il tempo specificato. Si tratta di un'attesa bloccante che consente di assicurarsi che tutte le attività siano completate prima di finalizzare il pool.
Inoltre, non abbiamo menzionato la principale interfaccia che aiuta a tracciare lo stato del thread, ovvero l'interfaccia Future. Il metodo submit() dell'interfaccia ExecutorService restituisce un'implementazione dell'interfaccia Future.
Se si desidera ottenere il risultato dell'esecuzione del thread, è possibile utilizzare il metodo get(). Se il thread implementa Runnable, il metodo get() non restituisce nulla, ma se implementa Callable<T>, restituisce un valore di tipo T.
Main.java
12345678910111213141516171819202122232425262728293031package com.example; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); // Callable task that returns a result Callable<String> task = () -> { Thread.sleep(1000); // Simulate some work return "Task result"; }; Future<String> future = executor.submit(task); try { // Get the result of the task String result = future.get(); System.out.println("Task completed with result: " + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } executor.shutdown(); } }
È inoltre possibile utilizzare il metodo cancel(boolean mayInterruptIfRunning) per tentare di annullare l'esecuzione di un task. Se il task non è ancora iniziato, verrà annullato. Se il task è già in esecuzione, potrebbe essere interrotto in base al flag mayInterruptIfRunning.
true: Se il task è in esecuzione, verrà interrotto chiamando Thread.interrupt() sul thread in esecuzione.
false: Se il task è in esecuzione, non verrà interrotto e il tentativo di annullamento non avrà effetto sul task attualmente in esecuzione.
E anche 2 metodi che intuitivamente si comprendono dal loro nome:
isCancelled(): Verifica se il task è stato annullato;isDone(): Verifica se il task è stato completato.
Esempio di utilizzo
La dimensione del thread pool dipende dalla natura dei task eseguiti. Generalmente, la dimensione del thread pool non dovrebbe essere codificata in modo statico; dovrebbe invece essere personalizzabile. La dimensione ottimale viene determinata monitorando il throughput dei task eseguiti.
È massimamente efficiente utilizzare un numero di threads = processor cores. Questo può essere verificato nel codice utilizzando Runtime.getRuntime().availableProcessors().
Main.java
1int availableProcessors = Runtime.getRuntime().availableProcessors();
Differenze tra la Creazione Diretta di Thread e l'utilizzo di ExecutorService
Le principali differenze tra la creazione diretta di thread e l'utilizzo di ExecutorService riguardano la comodità e la gestione delle risorse. La creazione manuale dei thread richiede la gestione individuale di ciascun thread, il che complica il codice e l'amministrazione.
ExecutorService semplifica la gestione utilizzando un thread pool, facilitando la gestione dei task. Inoltre, mentre la creazione manuale dei thread può portare a un elevato consumo di risorse, ExecutorService consente di personalizzare la dimensione del thread pool.
Grazie per i tuoi commenti!