2010-09-30 19 views
5

nel cercare di migliorare la mia comprensione su problemi di concorrenza, sto guardando il seguente scenario (Edit: ho cambiato l'esempio dall'elenco a runtime, che è più vicino a quello che sto cercando):Determinazione dell'ambito di sincronizzazione?

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); } 
    } 
} 

Allo stato attuale, ogni metodo è protetto da thread se usato standalone. Ora, quello che sto cercando di capire è come gestire dove la classe chiamata desidera chiamare:

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

Ma, come detto, non v'è alcuna garanzia che lo Stato sarà coerente tra la chiamata a getAll() e le chiamate a rimuovere(). Se più thread chiamano questo, sarò nei guai. Quindi la mia domanda è - Come dovrei abilitare lo sviluppatore a eseguire l'operazione in modo thread-safe? Idealmente, desidero rafforzare la sicurezza del thread in un modo che rende difficile per lo sviluppatore evitare/perdere, ma allo stesso tempo non complicato da raggiungere. Posso pensare a tre opzioni finora:

A: attiva il blocco 'this', quindi l'oggetto di sincronizzazione è accessibile al codice chiamante, che può quindi racchiudere i blocchi di codice. Svantaggio: Difficile da rispettare in fase di compilazione:

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

B: Inserire il codice di combinati in classe Esempio - e beneficiare di essere in grado di ottimizzare l'attuazione, come in questo caso. Svantaggio: dolore per aggiungere estensioni e potenziale combinazione di logica non correlata:

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

C: Fornire una classe di chiusura. Svantaggio: il codice in eccesso, potenzialmente troppo generoso di un blocco di sincronizzazione, potrebbe infatti rendere più facile deadlock:

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); 
      } 
     } 
    } 
); 

C'è qualcosa che mi manca? Come dovrebbe essere la sincronizzazione per garantire che il codice sia thread-safe?

risposta

2

Penso che a tutti manchi il suo vero problema. Quando si itera su una nuova matrice di Object e si tenta di rimuoverne uno alla volta il problema è ancora tecnicamente non sicuro (sebbene l'impianto di ArrayList non esploderebbe, non avrebbe avuto risultati attesi).

Anche con CopyOnWriteArrayList esiste la possibilità che sia presente una lettura non aggiornata nell'elenco corrente quando si tenta di rimuovere.

I due suggerimenti proposti sono soddisfacenti (A e B). Il mio suggerimento generale è B. Rendere sicure le collezioni è molto difficile. Un buon modo per farlo è dare al cliente il minimo di funzionalità possibile (entro limiti ragionevoli). Quindi offrire il metodo removeAll e rimuovere il metodo getAll sarebbe sufficiente.

Ora è possibile, allo stesso tempo, dire "bene, voglio mantenere l'API così com'è e lasciare che il cliente si preoccupi della sicurezza aggiuntiva del thread". Se questo è il caso, documento thread-sicurezza. Documenta il fatto che un'azione di 'ricerca e modifica' sia non atomica e non thread-safe.

Le attuali implementazioni di elenchi concorrenti sono tutte thread-safe per le singole funzioni offerte (get, remove add sono tutte thread-safe). Le funzioni composte non sono però e il meglio che si potrebbe fare è documentare come renderle thread-safe.

2

Penso che j.u.c.CopyOnWriteArrayList sia un buon esempio di problema simile che stai cercando di risolvere.

JDK ha avuto un problema simile con gli elenchi: c'erano diversi modi per sincronizzare su metodi arbitrari, ma senza sincronizzazione su più invocazioni (e ciò è comprensibile).

Quindi CopyOnWriteArrayList implementa effettivamente la stessa interfaccia ma ha un contratto molto speciale, e chiunque lo chiami, ne è a conoscenza.

Simile alla soluzione: è probabilmente necessario implementare List (o qualsiasi altra interfaccia sia questa) e allo stesso tempo definire contratti speciali per metodi esistenti/nuovi. Ad esempio, la coerenza di getAll non è garantita e le chiamate a .remove non falliscono se o è nullo, o non è all'interno dell'elenco, ecc. Se gli utenti desiderano sia opzioni combinate che sicure/coerenti, questa classe fornirà un metodo speciale che fa esattamente questo (es. safeDeleteAll), lasciando gli altri metodi più vicini al contratto originale possibile.

Quindi, per rispondere alla domanda, selezionerei l'opzione B, ma implementerei anche l'interfaccia che l'oggetto originale sta implementando.

1

Da Javadoc per elenco.toArray():

La matrice restituita sarà "sicuro" in che nessun riferimenti ad esso sono mantenuto da questa lista. (In altre parole , questo metodo deve allocare un nuovo array anche se questo elenco è supportato da una matrice). Il chiamante è quindi libero di modificare l'array restituito.

Forse non capisco cosa stai cercando di realizzare. Vuoi che l'array Object[] sia sempre sincronizzato con lo stato corrente di List? Per riuscirci, penso che dovresti sincronizzarti sull'istanza Example e tenere il blocco finché il tuo thread non ha terminato con la sua chiamata al metodo E qualsiasi altro Object[] array attualmente in uso. Altrimenti, come farai a sapere se l'originale List è stato modificato da un altro thread?

+0

Siamo spiacenti, l'esempio è stato un po 'incerto. Ho rimosso il riferimento a Elenco. Sto solo cercando di capire il modo migliore per affrontare: Blah result = example.method1(); example.method2 (risultato); dove la chiamata a method2 può causare problemi perché tra il metodo1 e il metodo2, un altro thread è andato e chiamato method3(). – Mike

3

Utilizzare uno ReentrantReadWriteLock esposto tramite l'API. In questo modo, se qualcuno deve sincronizzare diverse chiamate API, può acquisire un blocco al di fuori delle chiamate al metodo.

+0

Non è lo stesso dell'opzione A, utilizzando 'questo' come monitor di sincronizzazione? Vedo il beneficio del tuo suggerimento rendendo più difficile sincronizzarlo accidentalmente senza pensare. In tal caso, sarebbe: public Object getLock() {return ...; } ha più senso? – Mike

+0

Questo potrebbe anche funzionare poiché i blocchi sincronizzati sono anche rientranti (lo stesso thread può ottenere lo stesso blocco due volte senza bloccare). Il mio approccio ha il vantaggio che diversi thread possono chiamare 'get()' allo stesso tempo senza bloccare. –

1

È necessario utilizzare la granularità appropriata quando si sceglie cosa bloccare. Quello di cui ti lamenti nel tuo esempio è un livello di granularità troppo basso, in cui il lucchetto non copre tutti i metodi che devono accadere insieme. È necessario creare metodi che combinino tutte le azioni che devono accadere insieme nello stesso blocco.

I blocchi sono rientranti in modo che il metodo di alto livello possa chiamare metodi sincronizzati di basso livello senza problemi.

+0

Sembra un approccio ragionevole, in cui ho difficoltà è delineare i metodi la cui logica di business è meglio centrata altrove: dove le azioni multiple devono accadere all'interno dello stesso blocco, ma sono più complicate, ad es. non tutti gli elementi sono rimossi(), forse solo quelli che corrispondono a un Predicato sono? Penso di inserire un metodo 'public void remove (Predicate p)' nella classe Example, o permetto al lock di accedere al codice chiamante? – Mike

3

In generale, questo è un classico problema di progettazione multithread. Sincronizzando la struttura dei dati invece di sincronizzare i concetti che utilizzano la struttura dati, è difficile evitare il fatto che si abbia essenzialmente un riferimento alla struttura dati senza un blocco.

Suggerirei che i blocchi non vengano eseguiti così vicino alla struttura dati. Ma è un'opzione popolare.

Una tecnica potenziale per far funzionare questo stile è l'uso di un tree-walker di modifica. In sostanza, si espone una funzione che esegue una richiamata su ciascun 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} } } 
} 

Allora il ciclo diventa:

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

... 
    editAll(my_function); 
... 

L'azienda per cui lavoro (corensic) ha casi di test estratti da insetti reali per verificare che Jinx sta trovando gli errori di concorrenza correttamente. Questo tipo di blocco della struttura dei dati di basso livello senza sincronizzazione di livello superiore è piuttosto comune. Il callback per la modifica dell'albero sembra essere una soluzione popolare per questa condizione di gara.