Introdución
Co aumento do número de núcleos dispoñibles nos procesadores hoxe en día, ao longo do día, xunto Coa crecente necesidade de lograr un maior rendemento, as APIs multi-roscas son bastante populares. Java ofrece o seu propio marco multi-fío chamado Executor Marco.
Que é o executor do cadro Marco?
Executor Framework contén unha serie de compoñentes que se utilizan para xestionar de forma eficiente os fíos de traballo. A API do executor decou a execución da tarefa real de tarefa que será executada por Executors
. Este deseño é unha das implementacións do patrón de productor-consumidor.
o java.util.concurrent.Executors
Proporcione métodos de fábrica que se usan para crear ThreadPools
de fíos de traballo.
Para usar o cadro executor, necesitamos crear un deses grupos de subprocesos e enviar a tarefa de execución. É o traballo do programa Executor Framework e executa as tarefas enviadas e devolver os resultados do grupo de subproceses.
Unha pregunta básica que me vén á mente é por que necesitamos tales grupos de subprocesos cando podemos crear obxectos de java.lang.Thread
ou implementar Runnable
/ Callable
interfaces para acadar o paralelismo?
A resposta é reducida a dous feitos básicos:
- A creación dun novo fío para unha nova tarefa implica unha sobrecarga de creación e desmontaxe do fío. A xestión do ciclo de vida deste fío aumenta significativamente o tempo de execución.
- Engadir un novo fío para cada proceso sen ningún tipo de limitación que leva á creación dun gran número de fíos. Estes subprocesos ocupan a memoria e causan recursos de residuos. A CPU comeza a dedicar moito tempo a cambiar o contexto cando se intercambia cada fío e introdúcese outro fío para a execución.
Todos estes factores reducen o rendemento do sistema. Os grupos de subprocesados superan este problema mantendo vivos os fíos e reutilizándoos. Calquera exceso de tarefas que flúen a partir de que subprocesos no grupo poden manipular son gardados nun Queue
. Unha vez que se lanzan algún dos fíos, ocupan a seguinte tarefa desta cola. Esta cola de tarefas é esencialmente ilimitada para os executores listos para usar proporcionados polo JDK.
Tipos de executores
Agora que temos unha boa idea do que é un executor, imos Tome unha ollada tamén os diferentes tipos de executores.
Singlethredexecuctor
Este executor do grupo de subproceses só ten un fío. Utilízase para executar tarefas secuencialmente. Se o fío morre debido a unha excepción ao executar unha tarefa, créase un novo fío para substituír o antigo fío e as tarefas posteriores son executadas no novo.
ExecutorService executorService = Executors.newSingleThreadExecutor()
FixedThreadpool (n)
Como o seu nome indica, é un grupo de subprocesos dun número fixo de subprocesos. As tarefas enviadas ao executor son executadas polo n
e se hai máis tarefas almacénanse nun LinkedBlockingQueue
. Este número adoita ser o número total de subproceses soportados polo procesador subxacente.
ExecutorService executorService = Executors.newFixedThreadPool(4);
cachedthreadpool
Este grupo de subprocesos úsase principalmente cando Hai moitas tarefas paralelas de curta duración para executar. A diferenza do grupo de fíos fixos, o número de subprocesos neste grupo de executores non está limitado. Se todos os fíos están ocupados correndo algunhas tarefas e un novo chega, o grupo creará e engadirá un novo subproceso ao executor. Axiña que un dos subprocesos é gratuíto, retomará a execución das novas tarefas. Se un fío permanece inactivo por sesenta segundos, están rematados e eliminados da caché.
Con todo, se non se administra correctamente ou as tarefas non son de curta duración, o grupo de subprocesas terá moitos activos subprocesos. Isto pode levar á eliminación de recursos e, polo tanto, nunha caída de rendemento.
ExecutorService executorService = Executors.newCachedThreadPool();
programado fiscal
Este executor úsase cando nós Ter unha tarefa que debe ser executada a intervalos regulares ou se queremos atrasar unha determinada tarefa.
ScheduledExecutorService scheduledExecService = Executors.newScheduledThreadPool(1);
As tarefas poden ser programadas en ScheduledExecutor
usando calquera dos dous métodos scheduleAtFixedRate
ou scheduleWithFixedDelay
.
scheduledExecService.scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
scheduledExecService.scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit)
A principal diferenza entre os dous métodos é a súa interpretación do atraso entre as execucións consecutivas de traballo programado.
scheduleAtFixedRate
Executa a tarefa cun intervalo fixo, independentemente de que termine a tarefa anterior.
scheduleWithFixedDelay
iniciará a conta atrás do atraso só despois de completar a tarefa actual.
Comprender o obxecto futuro
Pode acceder ao resultado da tarefa enviada para a execución a un executor usando o obxecto
devolto polo executor. O futuro pode considerarse como unha promesa feita polo executor ao chamador.
Future<String> result = executorService.submit(callableTask);
Unha tarefa enviada ao executor, como a anterior, é asíncrona, É dicir, a execución do programa non espera a execución da tarefa de pasar ao seguinte paso. No seu canto, sempre que se complete a execución da tarefa, establécese neste Future
a través da Albacea.
O chamador pode continuar a executar o programa principal e Cando se necesita o resultado da tarefa enviada, pode chamar .get()
neste Future
. Se a tarefa está rematada, o resultado é inmediatamente devolto ao chamador ou, se non, o interlocutor está bloqueado ata que o executor complete a execución eo resultado calcúlase.
Se a persoa que a chamada non pode permitirse indefinidamente antes Recuperando o resultado, esta espera tamén pode ser programada. Isto é alcanzado polo método Future.get(long timeout, TimeUnit unit)
que produce un TimeoutException
se o resultado non se devolve dentro do período estipulado. O interlocutor pode manexar esta excepción e continuar coa posterior execución do programa.
Se hai unha excepción ao executar a tarefa, o método GET producirá un ExecutionException
.
Algo importante sobre o resultado devolto por é que se devolve só se a tarefa enviada implementa Se a tarefa implementa a interface
, a chamada a.get()
devolveránull
unha vez que o A tarefa está completa.
Outro método importante é o método
. Este método úsase para cancelar a execución dunha tarefa enviada. Se a tarefa xa está en execución, o executor intentará interromper a execución da tarefa se omayInterruptIfRunning
pasa a bandeira comotrue
.
Exemplo: Crear e executar un executor simple
Agora crearemos unha tarefa e trataremos de executalo nun executor de grupo fixo:
public class Task implements Callable<String> { private String message; public Task(String message) { this.message = message; } @Override public String call() throws Exception { return "Hello " + message + "!"; }}
o Task
Implements Callable
e está parametrizado para String
Tipo. Tamén é declarado lanzando Exception
. Esta capacidade de lanzar unha excepción ao executor e ao executor que devolve esta excepción ao chamador é de grande importancia porque axuda á persoa a chamar a coñecer o estado de execución da tarefa.
Agora executamos esta tarefa:
public class ExecutorExample { public static void main(String args) { Task task = new Task("World"); ExecutorService executorService = Executors.newFixedThreadPool(4); Future<String> result = executorService.submit(task); try { System.out.println(result.get()); } catch (InterruptedException | ExecutionException e) { System.out.println("Error occured while executing the submitted task"); e.printStackTrace(); } executorService.shutdown(); }}
Aquí creamos un executor
con un reconto de 4 fíos como se desenvolve esta demostración nun Procesador de catro núcleos. O número de subprocessos pode ser maior que os núcleos de procesador as tarefas que son executadas realizar operacións considerables E / ou tempo gasto esperando por recursos externos.
Temos instanciado o Task
clase e levala ao executor de execución. O resultado é devolto polo obxecto
, que logo imprime na pantalla.
Imos executar o ExecutorExample
e comprobar a súa saída:
Hello World!
Como esperaba, a tarefa engade o “Ola” saúdo e devolve o resultado a través do Future
Obxecto.
Finalmente, chamamos ao momento do executorService
para rematar todos os fíos e devolver recursos ao sistema operativo.
O .shutdown()
O método é actualmente a conclusión das tarefas que se envían actualmente ao executor. Non obstante, se o requisito está pechando inmediatamente o executor sen esperar, podemos usar a .shutdownNow()
no lugar.
Calquera tarefa de execución pendente será devolto nun java.util.List
.
Tamén podemos crear esta mesma tarefa implementando a interface Runnable
:
public class Task implements Runnable{ private String message; public Task(String message) { this.message = message; } public void run() { System.out.println("Hello " + message + "!"); }}
hai un par De cambios importantes aquí cando implementamos executable.
- O resultado da execución da tarefa non pode devolverse do método
run()
. Polo tanto, estamos a imprimir directamente desde aquí. - o O método non está configurado para lanzar ningunha excepción marcada.
Conclusión
O subproceso múltiple é cada vez máis común xa que a velocidade do reloxo do procesador é difícil de aumentar. Non obstante, manexar o ciclo de vida de cada fío é moi difícil debido á complexidade implicada.
Neste artigo, demostramos un marco múltiple múltiple múltiple múltiple, marco executor e explicou os seus diferentes compoñentes. Tamén botamos unha ollada a diferentes exemplos de creación e execución de tarefas nun executor.
Como sempre, o código deste exemplo pode atoparse en GitHub.