2012-04-10 24 views
8

Hay algo extraño acerca de la implementación del BoundedExecutor en el libro Java Concurrency in Practice.Concurrencia de Java en la práctica: condición de carrera en BoundedExecutor?

Se supone que debe estrangular el envío de tareas al Ejecutor al bloquear el hilo de envío cuando hay suficientes hilos en cola o ejecutándose en el Ejecutor.

Esta es la aplicación (después de añadir el volver a lanzar falta en la cláusula catch):

public class BoundedExecutor { 
    private final Executor exec; 
    private final Semaphore semaphore; 

    public BoundedExecutor(Executor exec, int bound) { 
     this.exec = exec; 
     this.semaphore = new Semaphore(bound); 
    } 

    public void submitTask(final Runnable command) throws InterruptedException, RejectedExecutionException { 
     semaphore.acquire(); 

     try { 
      exec.execute(new Runnable() { 
       @Override public void run() { 
        try { 
         command.run(); 
        } finally { 
         semaphore.release(); 
        } 
       } 
      }); 
     } catch (RejectedExecutionException e) { 
      semaphore.release(); 
      throw e; 
     } 
    } 

Cuando una instancia del BoundedExecutor con un Executors.newCachedThreadPool() y un límite de 4, yo esperaría que el número de hilos instanciados por el grupo de subprocesos en caché nunca excederá 4. En la práctica, sin embargo, sí lo hace. He conseguido este pequeño programa de prueba para crear hasta 11 hilos:

public static void main(String[] args) throws Exception { 
    class CountingThreadFactory implements ThreadFactory { 
     int count; 

     @Override public Thread newThread(Runnable r) { 
      ++count; 
      return new Thread(r); 
     }   
    } 

    List<Integer> counts = new ArrayList<Integer>(); 

    for (int n = 0; n < 100; ++n) { 
     CountingThreadFactory countingThreadFactory = new CountingThreadFactory(); 
     ExecutorService exec = Executors.newCachedThreadPool(countingThreadFactory); 

     try { 
      BoundedExecutor be = new BoundedExecutor(exec, 4); 

      for (int i = 0; i < 20000; ++i) { 
       be.submitTask(new Runnable() { 
        @Override public void run() {} 
       }); 
      } 
     } finally { 
      exec.shutdown(); 
     } 

     counts.add(countingThreadFactory.count); 
    } 

    System.out.println(Collections.max(counts)); 
} 

Creo que hay un pequeño marco de tiempo entre la liberación del semáforo y la terminación de tareas, donde otro hilo puede adquirir un permiso y envíe una tarea mientras el hilo de liberación aún no ha terminado. En otras palabras, tiene una condición de carrera.

¿Alguien puede confirmar esto?

+1

Agregué un 1ms Thread.sleep justo después del semáforo.release() para ver cuánto peor iba a obtener: obtuve más de 300 hilos creados. – toto2

Respuesta

2

Veo tanto como 9 hilos creados a la vez. Sospecho que hay una condición de carrera que hace que haya más hilo de lo necesario.

Esto podría deberse a que hay antes y después de ejecutar el trabajo de tarea por hacer. Esto significa que a pesar de que solo hay 4 subprocesos dentro de su bloque de código, hay una serie de subprocesos que detienen una tarea previa o se preparan para comenzar una nueva tarea.

es decir, el hilo tiene una liberación() mientras se está ejecutando. Aunque es lo último que haces, no es lo último que hace antes de adquirir una nueva tarea.

+0

Estoy de acuerdo con su declaración, que también es básicamente lo que sospechó Bossie en primer lugar. Pero no estoy seguro de que llamaría esto una condición de carrera, ya que implica algún error de programación: no creo que se suponga que el programa tenga un número máximo de hilos tal como está escrito. – toto2

+0

@ toto2 No es un error de programación regular, pero hay una condición de carrera entre el Semaphore que se lanza y el hilo que adquiere una nueva tarea. Si la tarea fue más duradera, es posible que solo vea este comportamiento con poca frecuencia. –

+1

Entiendo acerca de la duración de la tarea (ver mi comentario a la pregunta de Bossie). Solo quiero decir que no es un error porque no hay nada que especifique que debe haber 4 hilos. "¡No es un error, es una característica!" En este caso, creo que alguien realmente podría hacer esa afirmación. Lo siento ... solo me estoy volviendo filosófico aquí. – toto2

5

Tiene razón en su análisis de la condición de carrera. No hay garantías de sincronización entre ExecutorService & el semáforo.

Sin embargo, no sé si el límite del número de subprocesos se usa para BoundedExecutor. Creo que es más para estrangular el número de tareas enviadas al servicio. Imagine si tiene 5 millones de tareas que debe enviar, y si envía más de 10.000 de ellas, se quedará sin memoria.

Bueno, solo tendrá 4 subprocesos ejecutándose en un momento dado, ¿por qué querría intentar y poner en cola todas las tareas de 5 millones? Puede usar una construcción similar a esta para reducir el número de tareas puestas en cola en un momento dado. Lo que debe sacarse de esto es que en un momento dado solo hay 4 tareas ejecutándose.

Obviamente, la resolución de esto es usar un Executors.newFixedThreadPool(4).

+1

Exactamente. La tarea enviada al ejecutor subyacente que ejecuta 'command' libera el semáforo antes de que se complete, pero esto sucede en el subproceso ejecutor que ejecuta la tarea. Esto permite que otra tarea sea enviada al ejecutor y entregada a un nuevo hilo, que teóricamente permite el doble de hilos que 'bound 'dadas las condiciones adecuadas. –

+1

@John: regula el número de tareas enviadas, pero de forma impredecible y poco fiable. Si los hilos están programados de una manera desafortunada, esto podría significar que muchos subprocesos pueden colarse. Además, en el caso de un 'newFixedThreadPool()', estas tareas aún pueden acumularse en la cola sin límites del 'Ejecutor' , aún arriesgando la falta de memoria. @David: He conseguido hasta 11 hilos con un límite de 4. Además, creo que es gracioso que el libro de referencia sobre la concurrencia de Java todavía tenga condiciones de carrera. –

+0

@Bossie Tienes razón, sigues siendo sospechoso de OOM con una nueva FiixedThreadPool, pero eso evitaría que se crearan los hilos excedentes. Utiliza un límite para evitar, como mencioné, los millones de tareas que se envían al mismo tiempo. –

10

BoundedExecutor fue pensado como una ilustración de cómo acelerar el envío de tareas, no como una forma de colocar un límite en el tamaño del grupo de subprocesos. Hay formas más directas de lograr esto último, como se señaló al menos un comentario.

Pero las otras respuestas no mencionan el texto en el libro que dice utilizar una cola sin límites y para

set the bound on the semaphore to be equal to the pool size plus the number of queued tasks you want to allow, since the semaphore is bounding the number of tasks both currently executing and awaiting execution. [JCiP, end of section 8.3.3]

Al mencionar colas sin límites y el tamaño de la piscina, estábamos dando a entender (al parecer no es muy clara) el uso de un grupo de subprocesos de tamaño limitado.

Lo que siempre me ha molestado acerca de BoundedExecutor, sin embargo, es que no implementa la interfaz ExecutorService. Una forma moderna de lograr una funcionalidad similar y aún implementar las interfaces estándar sería utilizar el método listeningDecorator de Guava y la clase ForwardingListeningExecutorService.

+0

Una respuesta de uno de los expertos, eso es increíble. Gracias por aclarar un poco las cosas Tim. Esos 'ListenableFuture's se ven interesantes. Entonces, se supone que debo enviar un par de tareas, y cada vez que una completa, presento una nueva en la devolución de llamada, ¿verdad? –

+0

+1 de la fuente –

+0

@Bossie - Podría hacerlo, claro, pero quería decir que podría continuar utilizando el enfoque de semáforo de BoundedExecutorService decorando la ejecución con una adquisición y la devolución de llamada con un lanzamiento. –

Cuestiones relacionadas