Concorrencia Java: o marco de execución

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ánullunha 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 omayInterruptIfRunningpasa 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.

Deixa unha resposta

O teu enderezo electrónico non se publicará Os campos obrigatorios están marcados con *