2010-09-30 19 views
5

en tratar de mejorar mi comprensión de los problemas de concurrencia, estoy mirando el escenario siguiente (Editar: he cambiado el ejemplo de la Lista de tiempo de ejecución, que está más cerca de lo que estoy tratando):¿Determinar el alcance de sincronización?

public class Example { 
    private final Object lock = new Object(); 
    private final Runtime runtime = Runtime.getRuntime(); 
    public void add(Object o) { 
     synchronized (lock) { runtime.exec(program + " -add "+o); } 
    } 
    public Object[] getAll() { 
     synchronized (lock) { return runtime.exec(program + " -list "); } 
    } 
    public void remove(Object o) { 
     synchronized (lock) { runtime.exec(program + " -remove "+o); } 
    } 
} 

Tal como está, cada método es seguro mediante subprocesos cuando se usa de forma independiente. Ahora, lo que estoy tratando de averiguar es cómo manejar donde la clase que llama desee llamar:

for (Object o : example.getAll()) { 
    // problems if multiple threads perform this operation concurrently 
    example.remove(b); 
} 

Pero como se ha señalado, no hay ninguna garantía de que el Estado va a ser consistente entre la llamada a getAll() y las llamadas a eliminar(). Si hay varios hilos que llaman a esto, tendré problemas. Entonces mi pregunta es: ¿Cómo debo habilitar al desarrollador para que realice la operación de manera segura? Idealmente, deseo aplicar la seguridad del hilo de una manera que dificulte que el desarrollador evite/omita, pero al mismo tiempo no es complicado de lograr. Puedo pensar en tres opciones hasta ahora:

A: Haga que el bloqueo sea 'esto', de modo que el código de llamada acceda al objeto de sincronización, que luego puede envolver los bloques de código. Desventaja: Es difícil de cumplir en tiempo de compilación:

synchronized (example) { 
    for (Object o : example.getAll()) { 
     example.remove(b); 
    } 
} 

B: Coloque el código combinado en la clase Ejemplo - y beneficiarse de la posibilidad de optimizar la aplicación, como en este caso. Desventaja: Dolor para agregar extensiones, y el potencial de mezcla lógica no relacionada:

public class Example { 
    ... 
    public void removeAll() { 
     synchronized (lock) { Runtime.exec(program + " -clear"); } 
    } 
} 

C: proporcionar una clase de cierre. Desventaja: El exceso de código, potencialmente demasiado generoso de un bloque de sincronización, de hecho, podría hacer más fácil los puntos muertos:

public interface ExampleClosure { 
    public void execute(Example example); 
} 
public Class Example { 
    ... 
    public void execute(ExampleClosure closure) { 
     synchronized (this) { closure.execute(this); } 
    } 
} 

example.execute(new ExampleClosure() { 
     public void execute(Example example) { 
      for (Object o : example.getAll()) { 
       example.remove(b); 
      } 
     } 
    } 
); 

¿Hay algo que me falta? ¿Cómo se debe sincronizar la sincronización para garantizar que el código sea seguro para subprocesos?

Respuesta

2

Creo que a todos les falta su verdadero problema. Al iterar sobre la nueva matriz de Objetos y tratar de eliminar uno a la vez, el problema sigue siendo técnicamente inseguro (aunque la implantación de ArrayList no explotaría, simplemente no habría esperado resultados).

Incluso con CopyOnWriteArrayList existe la posibilidad de que haya una lectura desactualizada en la lista actual cuando intentas eliminarla.

Las dos sugerencias que ofreció son buenas (A y B). Mi sugerencia general es B. Hacer una colección segura para subprocesos es muy difícil. Una buena manera de hacerlo es darle al cliente la menor funcionalidad posible (dentro de lo razonable). Por lo tanto, sería suficiente ofrecer el método removeAll y eliminar el método getAll.

Ahora puedes decir al mismo tiempo, 'bueno, quiero mantener la API como está y dejar que el cliente se preocupe por la seguridad adicional de las secuencias'. Si ese es el caso, documente la seguridad del hilo. Documente el hecho de que una acción de 'búsqueda y modificación' es tanto atómica como no segura para subprocesos.

Las implementaciones de listas simultáneas de hoy en día son todas seguras para las funciones únicas que se ofrecen (get, remove add son todas seguras para subprocesos). Las funciones compuestas no se consideran y lo mejor que se puede hacer es documentar cómo hacer que sean seguras.

2

Creo que j.u.c.CopyOnWriteArrayList es un buen ejemplo de problema similar que está tratando de resolver.

JDK tenía un problema similar con las Listas: había varias formas de sincronizar en métodos arbitrarios, pero ninguna sincronización en múltiples invocaciones (y eso es comprensible).

En realidad, CopyOnWriteArrayList implementa la misma interfaz pero tiene un contrato muy especial, y quien lo llame, es consciente de ello.

Similar a su solución: probablemente debería implementar List (o la interfaz que sea) y al mismo tiempo definir contratos especiales para métodos existentes/nuevos. Por ejemplo, la consistencia de getAll no está garantizada, y las llamadas a .remove no fallan si o es nulo, o no está dentro de la lista, etc. Si los usuarios desean opciones combinadas y seguras/consistentes, esta clase de usted proporcionaría un método especial que hace exactamente eso (por ejemplo, safeDeleteAll), dejando otros métodos lo más cerca posible del contrato original.

Para responder a su pregunta, elegiría la opción B, pero también implementaría la interfaz que está implementando su objeto original.

1

Del Javadoc para la lista.toArray():

La matriz devuelta será "seguro" en que no hay referencias a ella son mantenida por esta lista. (En otras palabras , este método debe asignar una nueva matriz incluso si esta lista está respaldada por una matriz). La persona que llama es así libre de modificar la matriz devuelta.

Tal vez no entiendo lo que está tratando de lograr. ¿Desea que la matriz Object[] esté siempre sincronizada con el estado actual de List? Para lograr eso, creo que tendrías que sincronizar en la instancia Example misma y mantener el bloqueo hasta que el hilo termine con su llamada de método Y cualquier matriz Object[] que esté usando actualmente. De lo contrario, ¿cómo sabrá si el original List ha sido modificado por otro hilo?

+0

Disculpe, el ejemplo no estaba claro. He eliminado la referencia a la Lista. Solo estoy tratando de encontrar la mejor manera de lidiar con: Blah result = example.method1(); example.method2 (resultado); donde la llamada al método2 puede causar problemas porque entre el método1 y el método2, otro subproceso fue llamado método3(). – Mike

3

Utilice un ReentrantReadWriteLock que se expone a través de la API. De esta forma, si alguien necesita sincronizar varias llamadas API, puede adquirir un bloqueo fuera de las llamadas a métodos.

+0

¿No es lo mismo que la opción A, usando 'esto' como monitor de sincronización? Veo el beneficio de su sugerencia al hacer que sea más difícil sincronizar accidentalmente sin pensar. En ese caso, would: public Object getLock() {return ...; } ¿Tiene más sentido? – Mike

+0

Eso también podría funcionar, ya que los bloqueos sincronizados también son reentrantes (el mismo hilo puede obtener el mismo bloqueo dos veces sin bloquear). Mi enfoque tiene la ventaja de que varios hilos pueden llamar 'get()' al mismo tiempo sin bloquear. –

1

Tiene que usar la granularidad adecuada cuando elige qué bloquear. De lo que te quejas en tu ejemplo es un nivel de granularidad demasiado bajo, donde el bloqueo no cubre todos los métodos que tienen que suceder juntos. Debe crear métodos que combinen todas las acciones que deben suceder juntas dentro del mismo bloqueo.

Los bloqueos se vuelven a introducir, por lo que el método de alto nivel puede llamar sin problemas a los métodos sincronizados de bajo nivel.

+0

Parece un enfoque sensato, donde tengo dificultades para delinear métodos cuya lógica de negocios está mejor centrada en otro lugar: donde las múltiples acciones deben ocurrir dentro del mismo bloqueo, pero son más complicadas, p. no todos los elementos son remove(), tal vez solo los que coinciden con un predicado son? ¿Pienso poner en un método 'public void remove (Predicate p)' en la clase Example, o permito que el código de llamada acceda al bloqueo? – Mike

3

En general, este es un clásico problema de diseño multiproceso. Al sincronizar la estructura de datos en lugar de sincronizar los conceptos que usan la estructura de datos, es difícil evitar el hecho de que esencialmente tiene una referencia a la estructura de datos sin un bloqueo.

Yo recomendaría que los bloqueos no se hagan tan cerca de la estructura de datos. Pero es una opción popular.

Una técnica potencial para hacer que este estilo funcione es usar un editor de andadores de árbol. Esencialmente, expone una función que realiza una devolución de llamada en cada elemento.

// pointer to function: 
//  - takes Object by reference and can be safely altered 
//  - if returns true, Object will be removed from list 

typedef bool (*callback_function)(Object *o); 

public void editAll(callback_function func) { 
    synchronized (lock) { 
      for each element o { if (callback_function(o)) {remove o} } } 
} 

Así que el bucle se convierte en:

bool my_function(Object *o) { 
... 
    if (some condition) return true; 
} 

... 
    editAll(my_function); 
... 

La empresa donde trabajo (corensic) tiene casos de prueba extraídos de insectos reales para verificar que Jinx es encontrar los errores de concurrencia correctamente. Este tipo de bloqueo de estructuras de datos de bajo nivel sin una sincronización de nivel superior es un patrón bastante común. La devolución de llamada de edición de árbol parece ser una solución popular para esta condición de carrera.