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:
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!
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 foisAinsi, 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: 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; etsingle
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 ...
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() >]->