Concurrència en Java: el marc d’execució

Introducció

Amb l’augment en la quantitat de nuclis disponibles en els processadors avui dia, juntament amb la necessitat cada cop més gran d’aconseguir un major rendiment, les API de subprocessos múltiples s’estan tornant bastant populars. Java proporciona el seu propi marc de subprocessos múltiples anomenat Marc de l’executor.

Què és Executor Framework?

Executor Framework conté una sèrie de components que s’utilitzen per administrar de manera eficient els subprocessos de treball. L’API de l’executor desacobla l’execució de la tasca de la tasca real que s’executarà mitjançant Executors. Aquest disseny és una de les implementacions de l’Productor-consumidor patró.

els java.util.concurrent.Executors proporcionar mètodes de fàbrica que s’utilitzen per crear ThreadPools de fils de feina.

per a usar Executor Framework, necessitem crear un d’aquests grups de subprocessos i enviar-li la tasca per a la seva execució. És el treball de l’Executor Framework programar i executar les tasques enviades i retornar els resultats del grup de subprocessos.

Una pregunta bàsica que em ve al cap és per què necessitem tals grups de subprocessos quan podem crear objectes de java.lang.Thread o implementar Runnable / Callable interfícies per aconseguir el paral·lelisme?

La resposta es redueix a dos fets bàsics:

  • La creació d’un nou fil per a una nova tasca comporta una sobrecàrrega de creació i desmuntatge de el fil. La gestió de l’cicle de vida d’aquest fil augmenta significativament el temps d’execució.
  • Afegir un nou fil per a cada procés sense cap tipus de limitació condueix a la creació d’un gran nombre de fils. Aquests subprocessos ocupen memòria i provoquen el malbaratament de recursos. La CPU comença a dedicar massa temps a canviar de context quan s’intercanvia cada fil i entra un altre fil per a la seva execució.

Tots aquests factors redueixen el rendiment de el sistema. Els grups de subprocessos superen aquest problema mantenint vius els subprocessos i reutilitzant. Qualsevol excés de tasques que flueixin del que els subprocessos en el grup poden gestionar es mantenen en un Queue. Una vegada que algun dels fils s’allibera, reprenen la següent tasca d’aquesta cua. Aquesta cua de tasques és essencialment il·limitada per als executors preparats per utilitzar proporcionats pel JDK.

Tipus d’executors

Ara que tenim una bona idea del que és un executor, donem una cop d’ull també als diferents tipus d’executors.

SingleThreadExecutor

Aquest executor de grup de subprocessos té només un subprocés. S’utilitza per executar tasques de forma seqüencial. Si el fil mor a causa d’una excepció mentre s’executa una tasca, es crea un fil nou per reemplaçar el fil antic i les tasques posteriors s’executen en el nou.

ExecutorService executorService = Executors.newSingleThreadExecutor()

FixedThreadPool (n)

Com el seu nom indica, és un grup de subprocessos d’un nombre fix de subprocessos. Les tasques enviades a l’executor són executades pel n subprocessos i si hi ha més tasques s’emmagatzemen en un LinkedBlockingQueue. Aquest número sol ser el nombre total de subprocessos admesos pel processador subjacent.

ExecutorService executorService = Executors.newFixedThreadPool(4);

CachedThreadPool

Aquest grup de subprocessos s’usa principalment quan hi ha moltes tasques paral·leles de curta durada per a executar. A diferència del grup de subprocessos fixos, el nombre de subprocessos d’aquest grup d’executors no està limitat. Si tots els subprocessos estan ocupats executant algunes tasques i arriba una nova, el grup crearà i afegirà un nou fil a l’executor. Tan aviat com un dels subprocessos estigui lliure, reprendrà l’execució de les noves tasques. Si un fil roman inactiu durant seixanta segons, s’acaben i s’eliminen de la memòria cau.

No obstant això, si no s’administra correctament o les tasques no són de curta durada, el grup de subprocessos tindrà molts subprocessos actius . Això pot donar lloc a una eliminació de recursos i, per tant, a una caiguda de l’rendiment.

ExecutorService executorService = Executors.newCachedThreadPool();

Fiscal programat

Aquest executor s’usa quan tenim una tasca que necessita executar-se a intervals regulars o si volem retardar una determinada tasca.

ScheduledExecutorService scheduledExecService = Executors.newScheduledThreadPool(1);

Les tasques es poden programar en ScheduledExecutor usant qualsevol dels dos mètodes scheduleAtFixedRate o scheduleWithFixedDelay.

scheduledExecService.scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
scheduledExecService.scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit)

La principal diferència entre els dos mètodes és la seva interpretació de l’retard entre execucions consecutives d’un treball programat.

scheduleAtFixedRate executa la tasca amb un interval fix, independentment de quan va acabar la tasca anterior.

scheduleWithFixedDelay iniciarà el compte enrere de l’retard només després que es completi la tasca actual.

Comprendre l’objecte futur

es pot accedir a l’resultat de la tasca enviada per a la seva execució a un executor utilitzant el java.util.concurrent.Future objecte retornat per l’executor. El futur es pot considerar com una promesa feta per l’executor a la persona que truca.

Future<String> result = executorService.submit(callableTask);

Una tasca enviada a l’executor, com l’anterior, és asincrònica, és a dir, l’execució de el programa no espera que es completi l’execució de la tasca per passar a el següent pas. En canvi, sempre que es completa l’execució de la tasca, s’estableix en aquest Future objecte per part de l’marmessor.

La persona que truca pot continuar executant el programa principal i quan es necessita el resultat de la tasca enviada, pot trucar .get() en aquest Future objecte. Si la tasca es completa, el resultat es retorna immediatament a la persona que truca o, en cas contrari, la persona que truca es bloqueja fins que l’executor completi l’execució i es calculi el resultat.

Si la persona que truca no es pot permetre esperar indefinidament abans de recuperar el resultat, aquesta espera també pot cronometrar. Això s’aconsegueix mitjançant la Future.get(long timeout, TimeUnit unit) mètode que llança un TimeoutException si el resultat no es retorna en el termini estipulat. La persona que truca pot gestionar aquesta excepció i continuar amb l’execució posterior de el programa.

Si hi ha una excepció a l’executar la tasca, la crida a mètode get donarà un ExecutionException.

Una cosa important pel que fa a el resultat retornat per Future.get() mètode és que es retorna només si la tasca enviada implementa java.util.concurrent.Callable. Si la tasca implementa el Runnable interfície, la crida a .get() tornarà null una vegada que la tasca estigui completa.

Un altre mètode important és el Future.cancel(boolean mayInterruptIfRunning) mètode. Aquest mètode s’utilitza per cancel·lar l’execució d’una tasca enviada. Si la tasca ja s’està executant, l’executor intentarà interrompre l’execució de la tasca si el mayInterruptIfRunning la bandera es passa com true.

Exemple: crear i executar un executor simple

Ara crearem una tasca i intentarem executar-la en un executor de grup fix:

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 + "!"; }}

els Task implements de classe Callable i està parametritzat per String tipus. També es declara llençar Exception. Aquesta capacitat de llançar una excepció a l’executor i l’executor retornant aquesta excepció a la persona que truca és de gran importància perquè ajuda a la persona que truca a conèixer l’estat d’execució de la tasca.

Ara executem aquesta tasca:

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í hem creat un FixedThreadPool executor amb un compte de 4 subprocessos ja que aquesta demostració es desenvolupa en un processador de quatre nuclis. El nombre de subprocessos pot ser més gran que els nuclis de l’processador si les tasques que s’executen realitzen operacions d’E / S considerables o passen temps esperant recursos externs.

Hem instanciat el Task class i s’ho passen a l’executor per a la seva execució. El resultat és retornat pel Future objecte, que després imprimim a la pantalla.

Executem el ExecutorExample i verifiqui la seva sortida:

Hello World!

Com s’esperava, la tasca afegeix la salutació “Hola” i retorna el resultat a través del Future objecte.

Finalment, cridem a el tancament de l’executorService objecte per acabar tots els fils i tornar els recursos a sistema operatiu.

els .shutdown() el mètode espera la finalització de les tasques enviades actualment a l’executor. No obstant això, si el requisit és tancar immediatament l’executor sense esperar, podem fer servir el .shutdownNow() mètode al seu lloc.

Qualsevol tasca pendent d’execució es retornarà en un java.util.List objecte.

També podem crear aquesta mateixa tasca implementant el Runnable interfície:

public class Task implements Runnable{ private String message; public Task(String message) { this.message = message; } public void run() { System.out.println("Hello " + message + "!"); }}

Hi ha un parell de canvis importants aquí quan implementem runnable.

  • El resultat de l’execució de la tasca no es pot tornar des del run() mètode. Per tant, estem imprimint directament des d’aquí.
  • els run() El mètode no està configurat per a llançar cap excepció marcada.

Conclusió

el subprocés múltiple s’està tornant cada vegada més comú ja que la velocitat de l’ rellotge de l’ processador és difícil d’augmentar. No obstant això , manejar el cicle de vida de cada fil és molt difícil a causa de la complexitat involucrada.

En aquest article , vam demostrar un marc de subprocessos múltiple eficient però simple , Executor Framework, i expliquem els seus diferents components. També fem una ullada a diferents exemples de creació d’enviament i execució de tasques en un executor .

Com sempre , el codi d’aquest exemple es pot trobar a GitHub .

Deixa un comentari

L'adreça electrònica no es publicarà. Els camps necessaris estan marcats amb *