2010-07-22 13 views
6

"Java efectivo" de Joshua Bloch, el elemento 51 no se trata de depender del programador de subprocesos, así como de no mantener los hilos innecesariamente en el estado ejecutable. El texto citado:Java Concurrency JDK 1.6: ¿La espera ocupada es mejor que la señalización? Java efectiva # 51

La principal técnica para mantener el número de hilos ejecutables abajo es tener cada hilo hacer una pequeña cantidad de trabajo y luego esperar a alguna condición usando Object.wait o por alguna tiempo que debe transcurrir usando Thread.sleep. Los subprocesos no deben estar ocupados, espere, verifique repetidamente una estructura de datos esperando que ocurra algo. Además de hacer que el programa sea vulnerable a los caprichos del planificador, la espera ocupada puede aumentar en gran medida la carga del procesador, , reduciendo la cantidad de trabajo útil que otros procesos pueden realizar en la misma máquina.

Y luego continúa para mostrar una microbanda de una espera ocupada frente al uso de señales correctamente. En el libro, la espera ocupada hace 17 viajes circulares/s mientras que la versión esperar/notificar hace 23,000 viajes ida y vuelta por segundo.

Sin embargo, cuando probé el mismo benchmark en JDK 1.6, veo todo lo contrario: la espera agitada hace 760K recorridos de ida/vuelta mientras que la espera/notificación arroja 53.3K recorridos de ida/vuelta, es decir, espera/notificación debería ha sido ~ 1400 veces más rápido, pero resulta ser ~ 13 veces más lento?

Entiendo que las esperas ocupadas no son buenas y la señalización es aún mejor: la utilización de la CPU es ~ 50% en la versión de espera ocupada, mientras que permanece en ~ 30% en la versión de espera/notificación, pero ¿hay algo que explique ¿los números?

Si ayuda, estoy ejecutando JDK1.6 (32 bit) en Win 7 x64 (core i5).

ACTUALIZACIÓN: Fuente a continuación. Para ejecutar el banco de trabajo ocupado, cambie la clase base de PingPongQueue a BusyWorkQueue import java.util.LinkedList; import java.util.List;

abstract class SignalWorkQueue { 
    private final List queue = new LinkedList(); 
    private boolean stopped = false; 

    protected SignalWorkQueue() { new WorkerThread().start(); } 

    public final void enqueue(Object workItem) { 
     synchronized (queue) { 
      queue.add(workItem); 
      queue.notify(); 
     } 
    } 

    public final void stop() { 
     synchronized (queue) { 
      stopped = true; 
      queue.notify(); 
     } 
    } 
    protected abstract void processItem(Object workItem) 
     throws InterruptedException; 
    private class WorkerThread extends Thread { 
     public void run() { 
      while (true) { // Main loop 
       Object workItem = null; 
       synchronized (queue) { 
        try { 
         while (queue.isEmpty() && !stopped) 
          queue.wait(); 
        } catch (InterruptedException e) { 
         return; 
        } 
        if (stopped) 
         return; 
        workItem = queue.remove(0); 
       } 
       try { 
        processItem(workItem); // No lock held 
       } catch (InterruptedException e) { 
        return; 
       } 
      } 
     } 
    } 
} 

// HORRIBLE PROGRAM - uses busy-wait instead of Object.wait! 
abstract class BusyWorkQueue { 
    private final List queue = new LinkedList(); 
    private boolean stopped = false; 

    protected BusyWorkQueue() { 
     new WorkerThread().start(); 
    } 

    public final void enqueue(Object workItem) { 
     synchronized (queue) { 
      queue.add(workItem); 
     } 
    } 

    public final void stop() { 
     synchronized (queue) { 
      stopped = true; 
     } 
    } 

    protected abstract void processItem(Object workItem) 
      throws InterruptedException; 

    private class WorkerThread extends Thread { 
     public void run() { 
      final Object QUEUE_IS_EMPTY = new Object(); 
      while (true) { // Main loop 
       Object workItem = QUEUE_IS_EMPTY; 
       synchronized (queue) { 
        if (stopped) 
         return; 
        if (!queue.isEmpty()) 
         workItem = queue.remove(0); 
       } 

       if (workItem != QUEUE_IS_EMPTY) { 
        try { 
         processItem(workItem); 
        } catch (InterruptedException e) { 
         return; 
        } 
       } 
      } 
     } 
    } 
} 

class PingPongQueue extends SignalWorkQueue { 
    volatile int count = 0; 

    protected void processItem(final Object sender) { 
     count++; 
     SignalWorkQueue recipient = (SignalWorkQueue) sender; 
     recipient.enqueue(this); 
    } 
} 

public class WaitQueuePerf { 
    public static void main(String[] args) { 
     PingPongQueue q1 = new PingPongQueue(); 
     PingPongQueue q2 = new PingPongQueue(); 
     q1.enqueue(q2); // Kick-start the system 

     // Give the system 10 seconds to warm up 
     try { 
      Thread.sleep(10000); 
     } catch (InterruptedException e) { 
     } 

     // Measure the number of round trips in 10 seconds 
     int count = q1.count; 
     try { 
      Thread.sleep(10000); 
     } catch (InterruptedException e) { 
     } 
     System.out.println(q1.count - count); 

     q1.stop(); 
     q2.stop(); 
    } 
} 

Respuesta

6

En su prueba, la cola obtiene nuevos elementos de forma continua, por lo que el ocupado: espere hace muy poco de espera real.

Si la cola obtiene un elemento nuevo cada 1ms, puede ver que la espera ocupada gastará la mayor parte del tiempo en la quema de CPU para nada. Se ralentizará otra parte de la aplicación.

Así que depende. Si estás ocupado esperando una entrada de usuario, eso definitivamente es incorrecto; mientras que la espera ocupada en estructuras de datos sin cerradura como AtomicInteger es definitivamente buena.

+0

Creo que eso es todo. Solo lo probé. Con un sueño de 1 ms introducido antes de colocar el elemento en la otra cola, ambas ejecuciones son prácticamente iguales: alrededor de 400 recorridos de ida y vuelta. Como se esperaba, la espera ocupada consume hasta 3 veces más de la CPU. ¡Gracias! – Raghu

3

Sí, ocupado espera responderá con mayor rapidez y ejecutar más bucles, pero creo que el punto era que se pone una carga desproporcionadamente más pesado en todo el sistema.

Prueba a ejecutar 1000 hilos de espera ocupados frente a 1000 hilos de espera/notificación y verifica tu rendimiento total.

Creo que la diferencia que observa probablemente sea la optimización del compilador para lo que hace la gente en lugar de lo que la gente debería hacer. Sun hace eso todo el tiempo. El punto de referencia original en el libro puede haber sido debido a algún error del programador que Sun solucionó; con esa relación ciertamente suena mal.

+0

Bueno, el libro parece sugerir que 'siempre' pagas una penalización con esperas ocupadas, incluso cuando solo tienes un par de hilos. Además, los números citados indican exactamente eso. Veo una mayor utilización y definitivamente entiendo qué pasará si tienes suficientes otros hilos. Entonces, todavía desconcertado. – Raghu

1

Depende de la cantidad de subprocesos y del grado de conflictos: Las esperas de espera son malas, si ocurren con frecuencia y/o consumen muchos ciclos de CPU.

Pero los enteros atómicos (AtomicInteger, AtomicIntegerArray ...) son mejores que la sincronización de un entero o int [], incluso el subproceso también está ocupado esperando.

utilizar el paquete java.util.concurrent y en su caso ConcurrentLinkedQueueas menudo como sea posible

0

La espera ocupada no siempre es algo malo. La forma "correcta" (a bajo nivel) de hacer las cosas -utilizando las primitivas de sincronización de Java- conlleva una sobrecarga, a menudo significativa, de la contabilidad, necesaria para implementar mecanismos de propósito general, que rinde bastante bien en la mayoría de los escenarios. La espera ocupada, por otro lado, es muy liviana y, en algunas situaciones, puede ser una gran mejora con respecto a la sincronización única para todos. Si bien la sincronización basada únicamente en la espera ocupada es definitivamente un no-no en cualquier configuración general, es muy útil ocasionalmente. Es cierto no solo para Java: los "spinlocks" (nombres sofisticados para bloqueos basados ​​en espera ocupada) son ampliamente utilizados en servidores de bases de datos, por ejemplo.

De hecho, si das un paseo por las fuentes del paquete java.util.concurrent, encontrarás muchos lugares que contienen código "complicado", aparentemente frágil. Encuentro SynchronousQueue un buen ejemplo (puede echar un vistazo a la fuente en la distribución JDK o here, tanto OpenJDK como Oracle parecen usar la misma implementación). La espera ocupada se utiliza como una optimización: después de cierta cantidad de "giros", el hilo pasa a "dormir" correctamente. Aparte de eso, tiene algunas otras sutilezas también: volteos piggybacking, spin treshold depende de la cantidad de CPUs, etc. Es realmente ... esclarecedor, ya que muestra lo que se necesita para implementar una concurrencia eficiente de bajo nivel. Mejor aún, el código en sí mismo es realmente limpio, bien documentado y de alta calidad en general.

Cuestiones relacionadas