Una pratica operazione di confronto e scambio di più parole


10

Nel documento con lo stesso titolo di quella di questa domanda, gli autori descrivono come costruire un bloccante linearizzabile multi-word CAS operazione utilizzando solo una parola sola CAS. Inizialmente introducono l'operazione double-compare-single-swap - RDCSS, come segue:

word_t RDCSS(RDCSSDescriptor_t *d) {
  do {
    r = CAS1(d->a2, d->o2, d);
    if (IsDescriptor(r)) Complete(r);
  } while (IsDescriptor(r));
  if (r == d->o2) Complete(d); // !!
  return r;
}

void Complete(RDCSSDescriptor_t *d) {
  v = *(d->a1);
  if (v == d->o1) CAS1(d->a2, d, d->n2);
  else CAS1(d->a2, d, d->o2);
}

dove la RDCSSDescriptor_tè una struttura con i seguenti campi:

  • a1 - indirizzo della prima condizione
  • o1 - valore atteso al primo indirizzo
  • a2 - indirizzo della seconda condizione
  • o2 - valore atteso al secondo indirizzo
  • n2 - il nuovo valore da scrivere al secondo indirizzo

Questo descrittore viene creato e inizializzato una volta in un thread che avvia l'operazione RDCSS - nessun altro thread ha un riferimento ad esso fino a quando il primo CAS1 nella funzione non RDCSSriesce, rendendo raggiungibile il descrittore (o attivo nella terminologia del documento).

L'idea alla base dell'algoritmo è la seguente: sostituire la seconda posizione di memoria con un descrittore che dice cosa si desidera fare. Quindi, dato che il descrittore è presente, controlla la prima posizione di memoria per vedere se il suo valore è cambiato. In caso contrario, sostituire il descrittore nella seconda posizione di memoria con il nuovo valore. Altrimenti, reimpostare la seconda posizione di memoria sul vecchio valore.

Gli autori non spiegano perché la riga con il !!commento è necessaria all'interno del documento. Mi sembra che le CAS1istruzioni nella Completefunzione falliranno sempre dopo questo controllo, a condizione che non vi siano modifiche simultanee. E se ci fosse una modifica simultanea tra il check e il CAS in Complete, allora il thread che esegue il check dovrebbe comunque fallire con il suo CAS in Complete, poiché la modifica simultanea non dovrebbe usare lo stesso descrittore d.

La mia domanda è: può il controllo nella funzione RDCSSS, if (r == d->o2)...essere omesso, con RDCSS mantenendo la semantica di un confronto, di scambio di istruzioni singola doppia che è linearizzabile e senza blocchi ? (linea con !!commento)

In caso contrario, puoi descrivere lo scenario in cui questa riga è effettivamente necessaria per garantire la correttezza?

Grazie.


Innanzitutto, per capire cosa sta succedendo, dovremmo vedere la struttura dei dati RDCSSDescriptor_t. In secondo luogo, questo è probabilmente fuori tema qui in quanto non si occupa di informatica teorica; sarebbe meglio chiedere questo su stackoverflow.com.
Dave Clarke,

Il collegamento al documento è interrotto.
Aaron Sterling,

1
Mi scuso per il link: ora dovrebbe funzionare. Ho aggiornato la domanda per descrivere quale sia il descrittore. Il motivo per cui non l'ho pubblicato su StackOverflow.com è che le FAQ indicano che questo sito è destinato a domande a livello di ricerca in informatica. Ho pensato che le domande di libertà di blocco e linearizzabilità di un algoritmo si qualifichino come tali. Spero di aver capito le FAQ in modo errato.
axel22,

La parola chiave che hai perso nelle FAQ era "teorica". Poiché alcune persone trovano interessante la domanda, la lascio aperta.
Dave Clarke,

3
@Dave: non sono un esperto in questa sotto-area, ma per me sembra una domanda TCS molto tipica. Ti vengono dati due modelli di calcolo (A: con un CAS a parola singola, B: con un CAS a più parole) e una misura di complessità (numero di CAS) e ti viene chiesto se puoi simulare il modello B nel modello A, e con quale caso peggiore. (Qui potrebbe essere un po 'fuorviante che la simulazione sia data come un pezzo di codice C anziché pseudocodice; questo potrebbe suggerire a una persona teorica che questo è legato alle sfide della programmazione del mondo reale.)
Jukka Suomela,

Risposte:


9

In un ambiente di runtime simultaneo le cose semplici possono sembrare strane ... spero che questo possa aiutare.

Abbiamo un CAS1 ATOMICO INCORPORATO con questa semantica:

int CAS1(int *addr, int oldval, int newval) {
  int currval = *addr;
  if (currval == oldval) *addr = newval;
  return currval;
}

Dobbiamo definire una funzione ATOMIC RDCSS usando CAS1 e avendo la seguente semantica:

int RDCSS(int *addr1, int oldval1, int *addr2, int oldval2, int newval2) {
  int res = *addr;
  if (res == oldval2 && *addr1 == oldval1) *addr2 = newval2;
  return res;
}

Intuitivamente: dobbiamo cambiare CONCORRENTAMENTE il valore in addr2 solo se * addr1 == oldval1 ... se un altro thread lo sta cambiando, possiamo aiutare l'altro thread a completare l'operazione, quindi possiamo riprovare.

La funzione RDCSS verrà utilizzata (vedi articolo) per definire il CASN. Ora, definiamo un descrittore RDCSS nel modo seguente:

RDCSSDESCRI
int *addr1   
int oldval1
int *addr2   
int oldval2
int newval2

Quindi implementiamo RDCSS nel modo seguente:

int RDCSS( RDCSSDESCRI *d ) {
  do {
    res = CAS1(d->addr2, d->oldval2, d);  // STEP1
    if (IsDescriptor(res)) Complete(res); // STEP2
  } while (IsDescriptor(res);             // STEP3
  if (res == d->oldval2) Complete(d);     // STEP4
  return res;
}

void Complete( RDCSSDESCRI *d ) {
  int val = *(d->addr1);
  if (val == d->oldval1) CAS1(d->addr2, d, d->newval2);
    else CAS1(d->addr2, d, d->oldval2);  
}
  • PASSO1: prima proviamo a cambiare il valore di * addr2 nel nostro (proprio) descrittore d, se CAS1 ha successo allora res == d-> oldval2 (cioè res NON è un descrittore)
  • STEP2: controlla se res è un descrittore, ad esempio STEP1 non riuscito (un altro thread ha cambiato addr2) ... aiuta un altro thread a completare l'operazione
  • PASSAGGIO3: ritentare il PASSAGGIO1 se non si è riusciti a memorizzare il nostro descrittore d
  • PASSO 4: se abbiamo recuperato il nostro valore atteso da addr2, siamo riusciti a memorizzare il nostro descrittore (puntatore) in addr2 e possiamo completare il nostro compito memorizzando newval2 in * addr2 iif * addr1 == oldval1

RISPOSTA ALLA TUA DOMANDA

Se omettiamo STEP4, if (... && * addr1 == oldval1) * addr2 = newval2 parte della semantica RDCSS non verrà mai eseguita (... o meglio: può essere eseguita in modo imprevedibile da altri thread che aiutano quello attuale).

Come hai sottolineato nel tuo commento la condizione se (res == d1-> oldval2) in STEP4 non è necessario: anche se lo omettiamo, entrambi CAS1 in Complete () falliranno perché * (d-> addr2)! = D . Il suo unico scopo è evitare una chiamata di funzione.

Esempio T1 = thread1, T2 = thread2:

remember that addr1 / addr2 are in a shared data zone !!!

T1 enter RDCSS function
T2 enter RDCSS function
T2 complete STEP1 (and store the pointer to its descriptor d2 in addr2)
T1 at STEP1 the CAS1 fails and res = d2
T2 or T1 completes *(d2->addr2)=d2->newval2 (suppose that *(d2->addr1)==d2->oldval1)
T1 execute STEP1 and now CAS1 can fail because *addr2 == d2->newval2
   and maybe d2->newval2 != d1->oldval2, in every case at the end 
   res == d2->newval2 (fail) or
   res == d1->oldval2 (success)
T1 at STEP2 skips the call to Complete() (because now res is not a descriptor)
T1 at STEP3 exits the loop (because now res is not a descriptor)
T1 at STEP4 T1 is ready to store d1->newval2 to addr2, but only if
   *(d1->addr2)==d (we are working on our descriptor) and *(d1->addr1)==d1->oldval1
   ( Custom() function)

Grazie, buona spiegazione Ho completamente perso il punto che CAS1 restituisce il vecchio valore, non quello nuovo.
axel22,

Ma, nello scenario, le ultime 2 righe dicono che: senza la condizione in STEP4, T1 può memorizzare il valore, perché addr2contiene d2->newval2. Ma mi sembra che il CAS1 nel Completefallirà, perché si aspetta che il vecchio valore sia il descrittore d1- nulla sarà scritto da T1. Giusto?
axel22,

@ axel22: ho perso CAS1 in Complete () :-D. Sì hai ragione ... il mio esempio è sbagliato, la condizione if viene utilizzata solo per evitare una chiamata di funzione, se buttiamo via if () non cambia nulla. Ovviamente è necessario il completo (d) in STEP4. Ora modifico l'esempio.
Marzio De Biasi,

Evitare un CAS che ci aspettiamo di fallire è una tecnica di ottimizzazione della cache, per quanto ne so, poiché sull'hardware reale di solito ha effetti negativi come lo svuotamento delle linee della cache e l'acquisizione dell'accesso esclusivo a una linea della cache. Immagino che l'autore dell'articolo volesse che l'algoritmo fosse il più pratico possibile oltre che corretto.
Tim Seguine,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.