web-dev-qa-db-fra.com

différence entre omp critique et omp simple

J'essaie de comprendre la différence exacte entre #pragma omp critical et #pragma omp single dans OpenMP:

Les définitions de Microsoft pour ceux-ci sont:

  • Single: permet de spécifier qu'une section de code doit être exécutée sur un seul thread, pas nécessairement sur le thread principal.
  • Critique: spécifie que le code ne doit être exécuté que sur un thread à la fois.

Cela signifie donc que dans les deux, la section exacte de code sera ensuite exécutée par un seul thread et que les autres threads n'entreront pas dans cette section, par exemple si nous imprimons quelque chose, nous verrons le résultat à l'écran une fois, non?

Et la différence? Il semble que la critique s'occupe du temps d'exécution, mais pas unique! Mais je ne vois aucune différence dans la pratique! Cela signifie-t-il qu'une sorte d'attente ou de synchronisation pour d'autres threads (qui n'entrent pas dans cette section) est considérée comme critique, mais rien ne contient d'autres threads en single? Comment cela peut-il changer le résultat dans la pratique?

J'apprécie si quelqu'un peut me clarifier cela, surtout par un exemple. Merci!

26
Amir

single et critical sont deux choses très différentes. Comme vous l'avez mentionné:

  • single spécifie qu'une section de code doit être exécutée par un seul thread (pas nécessairement le thread principal)
  • critical spécifie que le code est exécuté par un thread à la fois

Ainsi, le premier sera exécuté ne seule fois tandis que le dernier sera exécuté autant de fois qu'il y a de threads.

Par exemple le code suivant

int a=0, b=0;
#pragma omp parallel num_threads(4)
{
    #pragma omp single
    a++;
    #pragma omp critical
    b++;
}
printf("single: %d -- critical: %d\n", a, b);

imprimera

single: 1 -- critical: 4

J'espère que vous voyez mieux la différence maintenant.

Par souci d'exhaustivité, je peux ajouter que:

  • master est très similaire à single avec deux différences:
    1. master sera exécuté par le maître uniquement tandis que single peut être exécuté par le thread atteignant la région en premier; et
    2. single a une barrière implicite à la fin de la région, où tous les threads attendent la synchronisation, tandis que master n'en a pas.
  • atomic est très similaire à critical, mais est limité pour une sélection d'opérations simples.

J'ai ajouté ces précisions car ces deux paires d'instructions sont souvent celles que les gens ont tendance à confondre ...

58
Gilles

single et critical appartiennent à deux classes complètement différentes de constructions OpenMP. single est une construction de partage de projet, aux côtés de for et sections. Les constructions de partage de projet sont utilisées pour distribuer une certaine quantité de travail entre les threads. De telles constructions sont "collectives" dans le sens où dans les programmes OpenMP corrects tous les threads doivent les rencontrent lors de l'exécution et de plus dans le même ordre séquentiel, y compris également les constructions barrier. Les trois conceptions de partage de projet couvrent trois cas généraux différents:

  • for (construction de boucle a.k.a.) distribue automatiquement les itérations d'une boucle entre les threads - dans la plupart des cas, tous les threads ont du travail à faire;
  • sections distribue une séquence de blocs de code indépendants entre les threads - certains threads font du travail. Il s'agit d'une généralisation de la construction for car une boucle avec 100 itérations pourrait être exprimée par exemple 10 sections de boucles avec 10 itérations chacune.
  • single sélectionne un bloc de code à exécuter par un seul thread, souvent le premier à le rencontrer (un détail d'implémentation) - un seul thread fonctionne. single est dans une large mesure équivalent à sections avec une seule section.

Un trait commun à toutes les constructions de partage de travail est la présence d'une barrière implicite à leur extrémité, laquelle barrière pourrait être désactivée en ajoutant le nowait à la construction OpenMP correspondante, mais la norme n'exige pas un tel comportement et avec certains runtimes OpenMP, la barrière peut continuer à être là malgré la présence de nowait. Les constructions de partage de travail mal ordonnées (c'est-à-dire hors séquence dans certains threads) peuvent donc entraîner des blocages. Un programme OpenMP correct ne bloquera jamais lorsque les barrières sont présentes.

critical est une construction de synchronisation, aux côtés de master, atomic et d'autres. Les constructions de synchronisation sont utilisées pour empêcher les conditions de concurrence et pour mettre de l'ordre dans l'exécution des choses.

  • critical empêche les conditions de concurrence en empêchant l'exécution simultanée de code parmi les threads du groupe de conflits dit . Cela signifie que tous les threads de tous les régions parallèles rencontrant des constructions critiques nommées de manière similaire sont sérialisés;
  • atomic transforme certaines opérations de mémoire simples en opérations atomiques, généralement en utilisant des instructions d'assemblage spéciales. Atomics se complète en une seule unité incassable. Par exemple, une lecture atomique d'un emplacement par un thread, qui se produit simultanément avec une écriture atomique au même emplacement par un autre thread, renverra soit l'ancienne valeur, soit la valeur mise à jour, mais jamais une sorte de mash-up intermédiaire de des bits des anciennes et des nouvelles valeurs;
  • master choisit un bloc de code pour l'exécution par le thread maître (thread avec l'ID de 0) uniquement. Contrairement à single, il n'y a pas de barrière implicite à la fin de la construction et il n'y a pas non plus d'exigence que tous les threads doivent rencontrer la construction master. De plus, l'absence de barrière implicite signifie que master ne vide pas la vue de la mémoire partagée des threads (c'est une partie importante mais très mal comprise d'OpenMP). master est essentiellement un raccourci pour if (omp_get_thread_num() == 0) { ... }.

critical est une construction très polyvalente car elle est capable de sérialiser différents morceaux de code dans des parties très différentes du code de programme, même dans différentes régions parallèles (significatif dans le cas du parallélisme imbriqué uniquement). Chaque construction critical a un nom facultatif fourni entre parenthèses immédiatement après. Les constructions critiques anonymes partagent le même nom spécifique à l'implémentation. Une fois qu'un thread entre dans une telle construction, tout autre thread rencontrant une autre construction du même nom est mis en attente jusqu'à ce que le thread d'origine quitte sa construction. Ensuite, le processus de sérialisation se poursuit avec le reste des threads.

Une illustration des concepts ci-dessus suit. Le code suivant:

#pragma omp parallel num_threads(3)
{
   foo();
   bar();
   ...
}

se traduit par quelque chose comme:

thread 0: -----< foo() >< bar() >-------------->
thread 1: ---< foo() >< bar() >---------------->
thread 2: -------------< foo() >< bar() >------>

(le fil 2 est délibérément un retardataire)

Avoir l'appel foo(); dans une construction single:

#pragma omp parallel num_threads(3)
{
   #pragma omp single
   foo();
   bar();
   ...
}

se traduit par quelque chose comme:

thread 0: ------[-------|]< bar() >----->
thread 1: ---[< foo() >-|]< bar() >----->
thread 2: -------------[|]< bar() >----->

Ici, [ ... ] Indique la portée de la construction single et | Est la barrière implicite à sa fin. Notez comment le thread retardataire 2 fait attendre tous les autres threads. Le thread 1 exécute l'appel foo() comme l'exemple d'exécution OpenMP choisit d'affecter le travail au premier thread pour rencontrer la construction.

L'ajout d'une clause nowait peut supprimer la barrière implicite, résultant en quelque chose comme:

thread 0: ------[]< bar() >----------->
thread 1: ---[< foo() >]< bar() >----->
thread 2: -------------[]< bar() >---->

Avoir l'appel foo(); dans une construction anonyme critical:

#pragma omp parallel num_threads(3)
{
   #pragma omp critical
   foo();
   bar();
   ...
}

se traduit par quelque chose comme:

thread 0: ------xxxxxxxx[< foo() >]< bar() >-------------->
thread 1: ---[< foo() >]< bar() >------------------------->
thread 2: -------------xxxxxxxxxxxx[< foo() >]< bar() >--->

Avec xxxxx... Est affiché le temps qu'un thread passe à attendre que d'autres threads exécutent une construction critique du même nom avant de pouvoir entrer sa propre construction.

Les constructions critiques de noms différents ne se synchronisent pas entre elles. Par exemple.:

#pragma omp parallel num_threads(3)
{
   if (omp_get_thread_num() > 1) {
     #pragma omp critical(foo2)
     foo();
   }
   else {
     #pragma omp critical(foo01)
     foo();
   }
   bar();
   ...
}

se traduit par quelque chose comme:

thread 0: ------xxxxxxxx[< foo() >]< bar() >---->
thread 1: ---[< foo() >]< bar() >--------------->
thread 2: -------------[< foo() >]< bar() >----->

À présent, le thread 2 ne se synchronise pas avec les autres threads car sa construction critique est nommée différemment et effectue donc un appel simultané potentiellement dangereux dans foo().

D'un autre côté, les constructions critiques anonymes (et en général les constructions portant le même nom) se synchronisent les unes avec les autres, peu importe où elles se trouvent dans le code:

#pragma omp parallel num_threads(3)
{
   #pragma omp critical
   foo();
   ...
   #pragma omp critical
   bar();
   ...
}

et le calendrier d'exécution résultant:

thread 0: ------xxxxxxxx[< foo() >]< ... >xxxxxxxxxxxxxxx[< bar() >]------------>
thread 1: ---[< foo() >]< ... >xxxxxxxxxxxxxxx[< bar() >]----------------------->
thread 2: -------------xxxxxxxxxxxx[< foo() >]< ... >xxxxxxxxxxxxxxx[< bar() >]->
27
Hristo Iliev