Une opération pratique de comparaison et d'échange de plusieurs mots


10

Dans l' article portant le même titre que celui de cette question, les auteurs décrivent comment construire une opération CAS multi-mots linéarisable non bloquante en utilisant uniquement un CAS en un seul mot. Ils introduisent d'abord l'opération de double comparaison-échange simple - RDCSS, comme suit:

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

RDCSSDescriptor_test une structure avec les champs suivants:

  • a1 - adresse de la première condition
  • o1 - valeur attendue à la première adresse
  • a2 - adresse de la deuxième condition
  • o2 - valeur attendue à la deuxième adresse
  • n2 - la nouvelle valeur à écrire à la deuxième adresse

Ce descripteur est créé et initialisé une fois dans un thread qui initie l'opération RDCSS - aucun autre thread n'y fait référence jusqu'à ce que le premier CAS1 de la fonction RDCSSréussisse, ce qui rend le descripteur accessible (ou actif dans la terminologie du papier).

L'idée derrière l'algorithme est la suivante - remplacez le deuxième emplacement de mémoire par un descripteur indiquant ce que vous voulez faire. Puis, étant donné que le descripteur est présent, vérifiez le premier emplacement de mémoire pour voir si sa valeur a changé. Si ce n'est pas le cas, remplacez le descripteur au deuxième emplacement mémoire par la nouvelle valeur. Sinon, redéfinissez le deuxième emplacement de mémoire sur l'ancienne valeur.

Les auteurs n'expliquent pas pourquoi la ligne avec le !!commentaire est nécessaire dans l'article. Il me semble que les CAS1instructions de la Completefonction échoueront toujours après cette vérification, à condition qu'il n'y ait pas de modification simultanée. Et s'il y avait une modification simultanée entre la vérification et le CAS dans Complete, alors le thread effectuant la vérification devrait toujours échouer avec son CAS dans Complete, car la modification simultanée ne devrait pas utiliser le même descripteur d.

Ma question est la suivante: la vérification de la fonction peut RDCSSS-elle if (r == d->o2)...être omise, RDCSS conservant toujours la sémantique d'une instruction de comparaison double et de permutation simple qui est linéarisable et sans verrouillage ? (ligne avec !!commentaire)

Sinon, pouvez-vous décrire le scénario où cette ligne est réellement nécessaire pour garantir l'exactitude?

Je vous remercie.


Premièrement, pour comprendre ce qui se passe, nous aurions besoin de voir la structure de données RDCSSDescriptor_t. Deuxièmement, c'est probablement hors sujet ici car il ne s'agit pas d'informatique théorique; il serait préférable de poser cette question sur stackoverflow.com.
Dave Clarke

Le lien vers le document est rompu.
Aaron Sterling

1
Je m'excuse pour le lien - cela devrait maintenant fonctionner. J'ai mis à jour la question pour décrire le descripteur. La raison pour laquelle je n'ai pas posté ceci sur stackoverflow.com est que la FAQ dit que ce site est destiné aux questions de recherche en informatique. Je pensais que les questions de liberté de verrouillage et de linéarisation d'un algorithme pouvaient être considérées comme telles. J'espère que j'ai mal compris la FAQ.
axel22

Le mot clé que vous avez manqué dans la FAQ était "théorique". Comme certaines personnes trouvent la question intéressante, je la laisse ouverte.
Dave Clarke

3
@Dave: Je ne suis pas un expert dans ce sous-domaine, mais pour moi, cela ressemble à une question TCS très typique. On vous donne deux modèles de calcul (A: avec un CAS à un seul mot, B: avec un CAS à plusieurs mots) et une mesure de complexité (nombre de CAS), et on vous demande si vous pouvez simuler le modèle B dans le modèle A, et avec quels frais généraux du pire des cas. (Ici, il peut être un peu trompeur que la simulation soit donnée comme un morceau de code C au lieu d'un pseudocode; cela pourrait suggérer à un
théoricien

Réponses:


9

Dans un environnement d'exécution simultanée, des choses simples peuvent sembler étranges ... j'espère que cela peut vous aider.

Nous avons un CAS1 ATOMIQUE INTÉGRÉ ayant cette sémantique:

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

Nous devons définir une fonction ATOMIC RDCSS en utilisant CAS1 et ayant la sémantique suivante:

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

Intuitivement: nous devons CHANGER ENCORE LA valeur à addr2 seulement si * addr1 == oldval1 ... si un autre thread le change nous pouvons aider l'autre thread à terminer l'opération, alors nous pouvons réessayer.

La fonction RDCSS sera utilisée (voir article) pour définir CASN. Maintenant, nous définissons un descripteur RDCSS de la manière suivante:

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

Ensuite, nous implémentons RDCSS de la manière suivante:

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);  
}
  • ÉTAPE1: nous essayons d'abord de changer la valeur de * addr2 en notre (propre) descripteur d, si CAS1 réussit alors res == d-> oldval2 (c'est-à-dire que res n'est PAS un descripteur)
  • STEP2: vérifier si res est un descripteur ie STEP1 a échoué (un autre thread a changé addr2) ... aidez un autre thread à terminer l'opération
  • STEP3: réessayez STEP1 si nous n'avons pas réussi à stocker notre descripteur d
  • ÉTAPE4: si nous avons récupéré notre valeur attendue dans addr2, nous avons réussi à stocker notre descripteur (pointeur) dans addr2 et nous pouvons terminer notre tâche en stockant newval2 dans * addr2 iif * addr1 == oldval1

RÉPONSE À VOTRE QUESTION

Si nous omettons STEP4, la partie if (... && * addr1 == oldval1) * addr2 = newval2 de la sémantique RDCSS ne sera jamais exécutée (... ou mieux: elle peut être exécutée de manière imprévisible par d'autres threads aidant l'actuel).

Comme vous l'avez souligné dans votre commentaire, la condition si (res == d1-> oldval2) à STEP4 n'est pas nécessaire: même si nous l'omettons, les deux CAS1 dans Complete () échoueront car * (d-> addr2)! = D . Son seul but est d'éviter un appel de fonction.

Exemple 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)

Merci, bonne explication. J'ai totalement raté le point que CAS1 renvoie l'ancienne valeur, pas la nouvelle.
axel22

Mais, dans le scénario, les 2 dernières lignes disent que: sans la condition à STEP4, T1 peut stocker la valeur, car addr2contient d2->newval2. Mais, il me semble que le CAS1 dans l' Completeéchec échouera, car il s'attend à ce que l'ancienne valeur soit le descripteur d1- rien ne sera écrit par T1. Droite?
axel22

@ axel22: J'ai raté le CAS1 dans Complete () :-D. Oui, vous avez raison ... mon exemple est faux, la condition if est utilisée uniquement pour éviter un appel de fonction, si nous jetons le if () rien ne change. Évidemment, le Complete (d) à STEP4 est nécessaire. Maintenant, je modifie l'exemple.
Marzio De Biasi

Éviter un CAS que nous prévoyons d'échouer est une technique d'optimisation du cache pour autant que je sache, car sur le matériel réel, il a généralement des effets négatifs tels que le vidage des lignes de cache et l'acquisition d'un accès exclusif à une ligne de cache. Je suppose que l'auteur de l'article voulait que l'algorithme soit aussi pratique que possible en plus d'être correct.
Tim Seguine
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.