¿Cómo puedo evitar esta limitación en la ThreadPoolExecutorque la cola debe estar limitada y llena antes de que se inicien más subprocesos?
Creo que finalmente he encontrado una solución algo elegante (tal vez un poco hacky) para esta limitación con ThreadPoolExecutor. Implica extenderlo LinkedBlockingQueuepara que vuelva falsepara queue.offer(...)cuando ya hay algunas tareas en cola. Si los subprocesos actuales no se mantienen al día con las tareas en cola, el TPE agregará subprocesos adicionales. Si el grupo ya está en el número máximo de subprocesos, RejectedExecutionHandlerse llamará al. Es el controlador el que luego ingresa put(...)a la cola.
Ciertamente es extraño escribir una cola en la que offer(...)pueda regresar falsey put()nunca se bloquee, así que esa es la parte del truco. Pero esto funciona bien con el uso de la cola por parte de TPE, por lo que no veo ningún problema al hacer esto.
Aquí está el código:
// extend LinkedBlockingQueue to force offer() to return false conditionally
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
private static final long serialVersionUID = -6903933921423432194L;
@Override
public boolean offer(Runnable e) {
// Offer it to the queue if there is 0 items already queued, else
// return false so the TPE will add another thread. If we return false
// and max threads have been reached then the RejectedExecutionHandler
// will be called which will do the put into the queue.
if (size() == 0) {
return super.offer(e);
} else {
return false;
}
}
};
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1 /*core*/, 50 /*max*/,
60 /*secs*/, TimeUnit.SECONDS, queue);
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
// This does the actual put into the queue. Once the max threads
// have been reached, the tasks will then queue up.
executor.getQueue().put(r);
// we do this after the put() to stop race conditions
if (executor.isShutdown()) {
throw new RejectedExecutionException(
"Task " + r + " rejected from " + e);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
});
Con este mecanismo, cuando envío tareas a la cola, la ThreadPoolExecutorvoluntad:
- Escale el número de subprocesos hasta el tamaño del núcleo inicialmente (aquí 1).
- Ofrézcalo a la cola. Si la cola está vacía, se pondrá en cola para ser manejada por los subprocesos existentes.
- Si la cola ya tiene 1 o más elementos,
offer(...)devolverá falso.
- Si se devuelve falso, aumente la cantidad de subprocesos en el grupo hasta que alcancen el número máximo (aquí 50).
- Si está al máximo, entonces llama al
RejectedExecutionHandler
- Los
RejectedExecutionHandlerpone entonces la tarea en la cola para ser procesados por el primer hilo disponible en orden FIFO.
Aunque en mi código de ejemplo anterior, la cola no está delimitada, también puede definirla como una cola delimitada. Por ejemplo, si agrega una capacidad de 1000 al, LinkedBlockingQueueentonces:
- escalar los hilos al máximo
- luego haga cola hasta que esté llena con 1000 tareas
- luego bloquee a la persona que llama hasta que haya espacio disponible para la cola.
Además, si necesita usar offer(...)en el
RejectedExecutionHandler, puede usar el offer(E, long, TimeUnit)método en su lugar con Long.MAX_VALUEel tiempo de espera.
Advertencia:
Si espera que las tareas se agreguen al ejecutor después de que se haya cerrado, entonces es posible que desee ser más inteligente al RejectedExecutionExceptioneliminar nuestra costumbre RejectedExecutionHandlercuando el servicio del ejecutor se haya cerrado. Gracias a @RaduToader por señalar esto.
Editar:
Otro ajuste a esta respuesta podría ser preguntar al TPE si hay subprocesos inactivos y solo poner el elemento en cola si es así. Tendría que hacer una clase verdadera para esto y agregarle un ourQueue.setThreadPoolExecutor(tpe);método.
Entonces su offer(...)método podría verse así:
- Verifique si el
tpe.getPoolSize() == tpe.getMaximumPoolSize()en cuyo caso simplemente llame super.offer(...).
- De lo contrario
tpe.getPoolSize() > tpe.getActiveCount(), llame super.offer(...)porque parece que hay hilos inactivos.
- De lo contrario, vuelva
falsea bifurcar otro hilo.
Tal vez esto:
int poolSize = tpe.getPoolSize();
int maximumPoolSize = tpe.getMaximumPoolSize();
if (poolSize >= maximumPoolSize || poolSize > tpe.getActiveCount()) {
return super.offer(e);
} else {
return false;
}
Tenga en cuenta que los métodos de obtención en TPE son costosos ya que acceden a volatilecampos o (en el caso de getActiveCount()) bloquean el TPE y recorren la lista de subprocesos. Además, aquí hay condiciones de carrera que pueden hacer que una tarea se ponga en cola de forma incorrecta o que se bifurque otro hilo cuando había un hilo inactivo.