|
|||||||
|
|
|
![]() |
|
|
Strumenti |
|
|
#21 |
|
Senior Member
Iscritto dal: Mar 2007
Messaggi: 7863
|
Ora la situazione si sta schiarendo ma resta ancora qualche dubbio e riprendo il tuo esempio di prima:
se abbiamo int[] array = new int[20]; la variabile array conterrà l' indirizzo, sull' heap del programma, in cui il vettore è ospitato. I thread che opera sull' array avranno una variabile di tipo reference array_privato che se ho ben capito(ed è proprio questo il dubbio), punterà (almeno inizialmente) proprio all' indirizzo sull' heap globale. Però a questo punto non capisco dove vengono fatte le scritture del tipo array[0]=1; che hanno scope locale ai thread. |
|
|
|
|
|
#22 |
|
Senior Member
Iscritto dal: Nov 2004
Città: Tra Verona e Mantova
Messaggi: 4553
|
Su variabili locali al thread, di tipo uguale a quello del componente dell'array, il cui valore iniziale è il valore predefinito dei campi non inizializzati.
Occhio che senza alcun intervento esterno quando l'array deve operare su "array" non è tenuto ad andare a vedere cosa c'è nella regione di memoria a cui punta la variabile array - cioè il valore della variabile. E' solo obbligato ad operare su una variabile del tipo di array. Cioè: int[] array = new int[20]; ... Thread 1: array[0] = 20; //può benissimo rilasciare una NullPointerException Mentre: final int[] array = new int[20]; ... Thread 1: array[0] = 20; //non rilascerà mai una NullPointerException Il final obbliga Thread 1: ad andare a guardare dove punta array, e non punta a null. Vedrà che ha dei campi. Non è obbligato ad andare a vedere il valore di quei campi. Quindi se io dicessi: final int[] array = new int[20]; array[0] = 300; ... Thread 1: print(array[0]); //Mai NullPointer ma può stampare zero anzichè 300 Vale anche per volatile, sebbene questo sia più "forte" di final: volatile int[] array = new int[20] array[0] = 300; Le specifiche dicono che la scrittura di un valore volatile happens-before la lettura di quel valore. "A write to a volatile variable v synchronizes-with all subsequent reads of v by any thread" Qui "v" è "array". Per la transitività, ciò che in uno stesso thread capita prima della scrittura di un valore volatile capita anche prima della lettura di quel valore. Ma è sempre lettura o scrittura del valore della variabile volatile. E se andiamo a guardare l'operazione di assegnamento ad un componente di un array vediamo che: 1. legge il valore "array" 2. non scrive sul valore "array" ma attraverso il valore array sul campo [0] C'è una lettura di "array" (il valore volatile) che precede una scrittura sul campo [0]. Quando, più tardi, accade: Thread 1: int y = array[0]; Essendo array volatile Thread 1 è obbligato a vedere il valore che array ha in forza dell'ultima scrittura. E l'ultima scrittura sulla variabile "array" è "new int[20]": la scrittura su array - e non tramite array - è il punto che, per una variabile volatile - definisce l'ordine di sincronizzazione. Paradossalmente, per via della semantica di volatile e della transitività, obbligheremmo Thread 1 a vedere 300 se dicessimo: volatile int[] array = new int[20]; array[0] = 300; array = array;//scrittura su array ... Thread 1: println(array[0]); //deve stampare 300 Deve stampare 300 perchè: 1. le azioni intra-thread capitano nell'ordine in cui appaiono nel codice. Quindi array[0] = 300; happens-before array = array. 2. la scrittura di un valore volatile da parte di un thread A happens-before la lettura su quel valore da parte di un thread B: quindi in Thread 1 "array" (la lettura) capita dopo array = array che capita dopo array[0]. in Thread 1, array[0] capita dopo la lettura di array. Assumendo che Thread 1 esegua le sue istruzioni dopo il thread che dichiara e e cambia il valore di array, il risultato che vedremo dovrà essere quello che avremmo se eseguissimo questo unico flusso: volatile int[] array = new int[20]; array[0] = 300; array = array;//scrittura su array println(array[0]); Effetto "accademico" di volatile perchè, come per l'incremento ++ o il decremento --, in un contesto più realistico non è possibile assumere che un Thread interverrà su array solo dopo "array = array" ma si introdurranno degli altri meccanismi di sincronizzazione che, in genere, garantiscono la visibilità e qualcos'altro.
__________________
Uilliam Scecspir ti fa un baffo? Gioffri Cioser era uno straccione? E allora blogga anche tu, in inglese come me! |
|
|
|
|
|
#23 | |
|
Senior Member
Iscritto dal: Mar 2007
Messaggi: 7863
|
Quote:
Quindi se io avessi ArrayList ar =new ArrayList(10); for(......) ar.add(...); e un thread deve operare un' aggiunta ar.add(new Integer(10)); potrebbe benissimo farla su una ArrayList vuota. Ultima modifica di nuovoUtente86 : 13-10-2009 alle 22:52. |
|
|
|
|
|
|
#24 |
|
Senior Member
Iscritto dal: Nov 2004
Città: Tra Verona e Mantova
Messaggi: 4553
|
Non è corretto. La regola è che un qualsiasi Thread è tenuto a vedere un qualsiasi campo inizializzato al suo valore di default (0, null, false).
Poi si va a vedere se sussistono delle condizioni particolari che obbligano il Thread a leggere il valore effettivo di quel campo. Vale per i campi singolarmente considerati. Se A è un campo attraverso cui è possibile accedere ad altri campi, un eventuale obbligo per un thread di vedere il valore attuale di A non comporta necessariamente l'obbligo di vedere i valori attuali dei campi accessibili tramite A.
__________________
Uilliam Scecspir ti fa un baffo? Gioffri Cioser era uno straccione? E allora blogga anche tu, in inglese come me! |
|
|
|
|
|
#25 | |
|
Senior Member
Iscritto dal: Nov 2004
Città: Tra Verona e Mantova
Messaggi: 4553
|
Quote:
Se per una qualche ragione (final, volatile o altro) è tenuto a vedere il valore di "ar" ciò non significa che sia tenuto a rispettare il valore dei campi accessibili tramite "ar".
__________________
Uilliam Scecspir ti fa un baffo? Gioffri Cioser era uno straccione? E allora blogga anche tu, in inglese come me! |
|
|
|
|
|
|
#26 |
|
Senior Member
Iscritto dal: Mar 2007
Messaggi: 7863
|
Quindi in realtà dichiarare una variabile reference volatile o sincronizzarla con qualche altro strumento del linguaggio garantisce che, un eventuale riassegnamento (ar=new ArrayList), sia visto da tutti i thread concorrenti ma non assicura nulla circa operazioni come add e remove ad esempio.
Mi viene ora in mente che le comuni implementazioni del produttore/consumatore(classi esempio di multithreading) potrebbero (in realtà quasi tutte le implementazioni poi funzionano) non funzionare per via dell' utilizzo del buffer private. Ma a questo punto come garantire che operazioni come array[0]=1; o arrayl.add(qualcheOggetto); abbiano effettivamente visibilità sull' heap globale? |
|
|
|
|
|
#27 |
|
Senior Member
Iscritto dal: Nov 2004
Città: Tra Verona e Mantova
Messaggi: 4553
|
E' corretto dire che volatile non garantisce in assoluto la visibilità delle mutazioni che coinvolgono riferimenti indirettamente accessibili attraverso la variabile volatile.
Tuttavia rendere quelle mutazioni visibili è piuttosto facile. Considera ad esempio synchronized. Il JMM dice che l'uscita da un blocco sincronizzato su un monitor M happens-before l'entrata in un blocco sincronizzato sullo stesso monitor M - da parte di un altro Thread. Supponiamo che due Thread condividano lo stesso monitor M, sia esso un riferimento ad hoc final Object M = new Object(); //sempre final o un Lock: final Lock M = new ReentrantLock(); o lo stesso reference che un Thread vuole passare ad un altro, a patto che ne sia un riferimento locale. Per via della transitività della relazione HB questo: final Object M = new Object(); 1.) Thread 1: Point p = new Point(10, 20); 2.) Thread 1: ArrayList<Point> list = new ArrayList<Point>(); 3.) Thread 1: list.add(p); 4.) Thread 1: synchronized(M) {} seguito da: 5.) Thread 2: synchronized(M) {} 6.) Thread 2: Point x = list.get(0); 7.) Thread 2: System.out.println("(" + p.x +"," + p.y + ")"); stampa sempre (10, 20) Proprio per via della transitivià. L'entrata di Thread 2 nel blocco sincronizzato è successiva, quanto alla visibilità delle letture e delle scritture, all'uscita da quel blocco da parte di Thread 1. Tutte le letture e scritture che Thread 1 esegue prima di uscire dal blocco sincronizzato avvengono in uno stesso thread quindi accadono nell'ordine in cui si vedono nel codice. Tutte le scritture e letture che Thread 2 esegue dopo l'entrata nel blocco sincronizzato sono parimenti operazioni eseguite in un solo thread quindi capitano nell'ordine del codice. E' la transitività della relazione happens-before a rendere le cose facili. Il problema è che la sincronizzazione richiede operazioni molto costose lato CPU - si parla delle più onerose in assoluto. Non è sempre necessaria. Ad esempio sappiamo dal JMM che l'azione che avvia un thread happens-before la prima azione (cioè la prima lettura o scrittura) eseguita dal nuovo Thread. Ciò significa che un caso come questo: Codice:
List<Point> list = new ArrayList<Point>(); //ipotetico campo
...crea un marea di punti e infilali in list
new Thread() {
public void run() {
for(Point p : list) {
fai qualcosa con p
}
}
}.start();
E ci sono poi le utilità del package collection. Una delle garanzie di ConcurrentLinkedQueue o di CopyOnWriteArrayList eccetera è che i "put" e gli "add" capitano prima dei "get". Quindi data un'ipotetico campo-coda: final ConcurrentLinkedQueue<Point> coda = new ConcurrentLinkedQueue<Point>(); Thread 1: Point p = new Point(); Thread 1: p.x = una tragedia di conti che coinvolge milioni di altri campi Thread 1: coda.offer(p); e letto in seguito da un altro Thread 2: Point p = coda.take(); è "correttamente sincronizzato", nel senso che i valori x e y, indirettamente accessibili tramite p, saranno letti per il valore che effettivamente hanno nell'heap. perchè il campo coda deve essere final? Perchè ConcurrentLinkedQueue dice che "take" capita dopo "offer" ma prima di "take" Thread 2 compie un'azione, la lettura del campo "coda", e questa non è successiva al take. A conti fatti bisogna semplicemente prestare attenzione a qualche piccolo dettaglio ma il grosso della creazione di programmi multithread Java correttamente sincronizzati è relativamente gestibile. E non c'è il problema di andare a guardare come funzioni questo o quel compilatore, questa o quella architettura.
__________________
Uilliam Scecspir ti fa un baffo? Gioffri Cioser era uno straccione? E allora blogga anche tu, in inglese come me! |
|
|
|
|
|
#28 | ||
|
Senior Member
Iscritto dal: Mar 2007
Messaggi: 7863
|
Quote:
Quote:
Perchè l' happens before ci assicura l' ordine delle azione, ma non credo che indichi nulla circa la località o globalità dei dati. Ultima modifica di nuovoUtente86 : 14-10-2009 alle 01:45. |
||
|
|
|
|
|
#29 |
|
Senior Member
Iscritto dal: Nov 2004
Città: Tra Verona e Mantova
Messaggi: 4553
|
Mhh... non ho capito la domanda
![]() Dal punto di vista del linguaggio la ragione per cui quel codice, nel presupposto che Thread 2 esegua quelle istruzioni dopo Thread 1, è correttamente sincronizzato, deriva dal fatto che sussitono le relazioni happens-before citate. Spetta poi alla macchina virtuale fare in modo che quelle regole del linguaggio di programmazione Java siano "fisicamente" rispettate. La località è metaforica. Una JVM che semplicemente dica ai Thread di andare sempre e comunque ad aggiornare la memoria condivisa rispetta il modello di memoria. E non ci sarebbe alcuna località. Ma anche una JVM che solo sull'offer e sul take emettesse un'istruzione (mfence o mf o mb o msync) rispetterebbe l'happens-before mentre le istruzioni precedenti e successive sarebbero libere di agire sui valori eventualmente contenuti nella cache del processore. Non so però se questo fosse il dubbio.
__________________
Uilliam Scecspir ti fa un baffo? Gioffri Cioser era uno straccione? E allora blogga anche tu, in inglese come me! |
|
|
|
|
|
#30 | ||
|
Senior Member
Iscritto dal: Mar 2007
Messaggi: 7863
|
Quote:
Codice:
An action that starts a thread synchronizes-with the first action in the thread it starts. The write of the default value (zero, false or null) to each variable synchronizes-with the first action in every thread. Although it may seem a little strange to write a default value to a variable before the object containing the variable is allocated, conceptually every object is created at the start of the program with its default initialized values. spero di non aver detto schiocchezze. Quote:
Quello che mi viene da pensare è che le regole di happens-before costituiscono punto di sincronizzazione. |
||
|
|
|
|
|
#31 | |
|
Senior Member
Iscritto dal: Nov 2004
Città: Tra Verona e Mantova
Messaggi: 4553
|
Il perchè è, banalmente, perchè lo dice la javadoc, cioè fa parte del contratto delle collezioni concurrent. Poi se andiamo a guardare il codice, ad esempio di CopyOnWriteArrayList, vediamo che per certe operazioni usa un Lock, il cui contratto dichiara un'equivalenza semantica con synchronized(X), per altre si avvale di condizioni happens-before stabilite nel JMM che non richiedono l'uso di modificatori o primitive di sincronizzazione (un po' come thread.start() è sincronizzato con la prima lettura o scrittura eseguita dal thread senza il bisogno di ulteriori aggiunte).
Lo stesso fa coda.offer. Codice alla mano, usa AtomicReferenceUpdater che è una "capsula" in grado di garantire la visibilità inter thread delle mutazioni occorse ai campi di un dato riferimento. Ma sono tutti dettagli di realizzazione, quello che interessa a noi "utenti", del linguaggio o delle sue librerie, è che talune operazioni sono, in senso lato, "thread-safe". Quote:
Di queste talune sono intra-thread, talaltre inter-thread. A certe condizioni (lo start, il final, il volatile, il synchronized, lo yield, il wait-notify eccetera) le azioni inter-thread sono da considerarsi sincronizzate. Da ultimo, se un'azione A è sincronizzata con un'azione B allora l'implementazione deve fare in modo che l'esecuzione del programma manifesti l'accadimento di A prima di B. Come è irrilevante: se un thead dice che x vale uno e un thread dice che x vale x + 1 e dal codice sorgente risulta che queste due espressioni accadono una prima dell'altra - perchè comportano azioni, quelle azioni sono sincronizzate, e quindi A happens-before B - allora in un qualsiasi modo alla fine x dovrà valere 2.
__________________
Uilliam Scecspir ti fa un baffo? Gioffri Cioser era uno straccione? E allora blogga anche tu, in inglese come me! |
|
|
|
|
|
|
#32 |
|
Senior Member
Iscritto dal: Mar 2007
Messaggi: 7863
|
Codice:
public class Test{
public static void main(String[]args){
int[] array={10,10,10,10,10};
new ProvaThread(array,1).start();
new ProvaThread(array,2).start();
}}
Se però invece di una stampa facessi un' assegnamento array[1]=7; che comportamento si avrebbe? e ancora se modificassi il codice con qualcosa del genere Codice:
public class Test{
public static void main(String[]args){
int[] array=new int[10];
Thread t1=new ProvaThread(1).start();
Thread t2=new ProvaThread(2).start();//codice non corretto ma rende l' idea dello start
array[x]=10;
.
.
.riempimento dell' array
t1.setArray(array);
t2.setArray(array);
}}
|
|
|
|
|
|
#33 |
|
Senior Member
Iscritto dal: Nov 2004
Città: Tra Verona e Mantova
Messaggi: 4553
|
Direi che è corretto, anche se il secondo caso potrei non averlo capito tanto bene.
Il primo è correttamente sincronizzato per via dello "start": ciò che è stato fatto prima dello start è visibile nel run del thread. Se intendo correttamente il secondo caso, anche qui la tua deduzione è corretta. Il main cambia un valore dell'array dopo lo start dei thread. C'è ancora il punto di sincronizzazione nello start solo che stavolta c'è anche una scrittura successiva a quel punto a fronte della quale lo "start" non è più rilevante. Al che sorge l'interrogatorio: se volessimo obbligare i due thread a vedere quel valore 10 per il componente x di array, come potremmo fare?
__________________
Uilliam Scecspir ti fa un baffo? Gioffri Cioser era uno straccione? E allora blogga anche tu, in inglese come me! |
|
|
|
|
|
#34 |
|
Senior Member
Iscritto dal: Mar 2007
Messaggi: 7863
|
Una soluzione l' avevi proposta qualche post fa: rendere volatile array e fare un assegnamento array=array. Oppure usare qualche meccanismo esplicito di locking.
Nel primo blocco di codice postato,non mi è chiarissimo cosa avviene se i thread invece di una stampa cambiano qualche cella dell' array. |
|
|
|
|
|
#35 | |
|
Senior Member
Iscritto dal: Oct 2007
Città: Padova
Messaggi: 4131
|
Quote:
(Scherzo, sto seguendo con molto interesse il thread
__________________
As long as you are basically literate in programming, you should be able to express any logical relationship you understand. If you don’t understand a logical relationship, you can use the attempt to program it as a means to learn about it. (Chris Crawford) |
|
|
|
|
|
|
#36 | |
|
Senior Member
Iscritto dal: Nov 2004
Città: Tra Verona e Mantova
Messaggi: 4553
|
Quote:
Main-Thread: crea l'array e lo inizializza con dei valori. Questa inizializzazione comporta la scrittura di valori sull'heap Main-Thread: avvia Thread 1. L'avvio è sincronizzato con le azioni precedenti che, quindi, happen-before la prima azione di Thread 1 Main-Thread: avvia Thread 2. Come sopra. Thread 1: legge un campo - un componente dell'array. E' un'azione inter-thread. Abbiamo visto che è sincronizzata con la scrittura di Main-Thread. Thread 2: legge un campo, vedi sopra. Ora supponiamo che Thread 1 scriva su un componente dell'array. Thread 1: array[3] = 20; Naturalmente dal punto di vista di Thread 1 non c'è nessuno problema. Se per caso Thread 1 dovesse successivamente leggere il valore di array[3] vedrà 20 perchè le azioni intra-thread - cioè le azioni compiute all'interno di uno stesso thread - hanno l'effetto che dovrebbero avere secondo l'ordine di apparizione nel codice. Ma cosa succederebbe se in seguito Thread 2 leggesse il valore di array[3]? Abbiamo la garanzia che veda 20? O potrebbe vedere 10? Per stabilire se sia certo che Thread 2 veda 20 dobbiamo poter rinvenire nel codice una relazione happens-before tra la scrittura: Thread 1: array[3] = 20; e la successiva ipotetica lettura: Thread 2: print(array[3]); Questa relazione tuttavia non c'è. C'è una relazione tra ciò che accade prima di start nel Thread Main e ciò che accade dopo start in Thread 1 e Thread 2 ma non c'è tra ciò che capita in Thread 1 e 2 e quello che Main fa dopo averli avviati. La mancanza di questa relazione ci fa dire che il programma non è correttamente sincronizzato e il risultato è indeterminato. Nella fattispecie sono legittime sia la lettura del valore 10, sia la lettura del valore 20. Non è invece ammesso che array risulti null o che i suoi componenti valgano zero.
__________________
Uilliam Scecspir ti fa un baffo? Gioffri Cioser era uno straccione? E allora blogga anche tu, in inglese come me! |
|
|
|
|
|
|
#37 | |
|
Senior Member
Iscritto dal: Mar 2007
Messaggi: 7863
|
Era quello che mi aspettavo infatti.
Quote:
|
|
|
|
|
|
|
#38 |
|
Senior Member
Iscritto dal: Nov 2004
Città: Tra Verona e Mantova
Messaggi: 4553
|
I thread java sono tutti uguali. Il thread "main" non fa eccezione.
Tieni conto che in Java il metodo è l'unità di esecuzione dei Thread, cioè quando vedi un metodo sai che c'è un thread che lo sta eseguendo. Il metodo main è un metodo, quindi le istruzioni che contiene sono eseguite da un Thread. la necessità di sincronizzare discende da questo fatto. Quando nel metodo main troviamo: Codice:
public static void main(String[] args) {
int[] array = {10, 10, 10, 10, 10};
Dopo c'è un: new ProvaThread(array, 1).start(); quindi sappiamo che un Thread sicuramente diverso da quello che ha eseguito la prima riga farà qualcosa su array. Ti confermo che il concetto dell'area privata di lavoro è solo metaforico: rende l'idea del perchè sarebbe possibile che un thread non veda un certo valore che nel codice sembra invece essere chiaramente determinato.
__________________
Uilliam Scecspir ti fa un baffo? Gioffri Cioser era uno straccione? E allora blogga anche tu, in inglese come me! |
|
|
|
|
|
#39 | |
|
Senior Member
Iscritto dal: Mar 2007
Messaggi: 7863
|
Ma l' effettiva sincronizzazione avviene appena prima di start(), indipendentemente che poi il thread operi o meno sull' array(starto un thread che stampa ciao e poi muore), oppure è legata al fatto che il thread sicuramente andrà ad operarci su?
Quote:
|
|
|
|
|
|
|
#40 |
|
Senior Member
Iscritto dal: Nov 2004
Città: Tra Verona e Mantova
Messaggi: 4553
|
La sincronizzazione viaggia sempre in coppia: servono due azioni una compiuta da un Thread e una compiuta da un'altro Thread.
Ad esempio quando un Thread esce da un blocco synchronized, cioè che ha fatto fino ad allora è visibile al Thread che successicavamente entri in un blocco sincronizzato sullo stesso monitor. Non c'è, per intenderci, una regola che dica: ok, dopo aver fatto questo il Thread aggiorna la metaforica regione di memoria condivisa. No, è sempre una questione di rapporto tra almeno due thread: dopo aver fatto questo, ciò che è stato fatto prima sarà visibile a qualcun'altro. E' importante tenerlo presente altrimenti uno potrebbe pensare, come pare qualcuno faccia, che basti un punto di sincronizzazione non condiviso a rendere visibile l'operato di un thread: Thread 1: scrivi, leggi, modifica e trallallà Thread 1: synchronized(X) {} //PAF, il Thread "scarica" nell'HEAP NO, non funziona così. Vale solo nei confronti di un secondo Thread per cui esista un vincolo hb col primo. Quanto all'implementazione, non ho la più pallida idea di come sia fatta. Sì, in generale ci sono queste istruzioni nell'architettura di una CPU che sembrano essere in grado di stabilire se un valore debba essere preso presumo dalla cache piuttosto che dalla memoria principale. Poi si narra che ogni thread abbia la sua pila di "frame", uno per ogni metodo in esecuzione, che sono delle strutture dati che contengono una copia delle variabili chiamate in causa dall'esecuzione di un metodo. Ma se vieni a chiedere dove sia allocata la memoria usata da questi frame per me potrebbe benissimo stare tra Nettuno e Plutone. Comunque ci sono i sorgenti della JVM nel progetto OpenJDK. Una paio di volte ho anche provato a guardarli ma, francamente, più che capirci qualcosa direi di essere rimasto lì in affascinata contemplazione.
__________________
Uilliam Scecspir ti fa un baffo? Gioffri Cioser era uno straccione? E allora blogga anche tu, in inglese come me! |
|
|
|
|
| Strumenti | |
|
|
Tutti gli orari sono GMT +1. Ora sono le: 14:40.





















