Skoči na vsebino

Sodelovanje med nitmi

Niti posamezne skupine, ki je ustvarjena z ukazom #pragma omp parallel, se izvajajo neodvisno, a večinoma sodelujejo pri reševanju istega problema. To pomeni, da morajo med seboj sodelovati in komunicirati. Komunikacija med nitmi poteka v glavnem preko skupnih spremenljivk, pri tem pa se skrivajo različne pasti. Kot primer si poglejmo naslednji program, ki sešteje oznake vseh niti:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <omp.h>

int main (int argc, char *argv[])
{
  int nt; sscanf (argv[1], "%d", &nt);

  int sum = 0;
  #pragma omp parallel num_threads(nt)
  {
    int n = omp_get_thread_num ();
    sum = sum + n;
    printf ("%d: %d\n", n, sum);
  }
  printf ("%d\n", sum);
  return 0;
}

Spremenljivka sum je uporabljena kot skupna spremenljivka, ki služi za komunikacijo med nitmi: vsaka nit tej spremenljivki prišteje svojo oznako, a program kljub temu ne deluje pravilno. Še več, večina niti ob koncu ipiše povsem napačno vsoto oznak.

Vaja

Prepričajte se, da program error-1.c res deluje napačno.

Do napake pride zato, ker v 12. vrstici več niti hkrati izvede prištevanje oznake isti spremenljivki. Več niti torej hkrati prebere iz pomnilnika vrednost spremenljivke sum, ji prišteje svojo oznako in zapiše rezultat nazaj v pomnilnik. Seveda se ohrani le zadnji zapis v pomnilnik, ostali so izgubljeni.

Atomarna operacija

En način reševanja problema, ko več niti hkrati izvaja operacijo nad skupno spremenljivko, je izvedba te operacije v enem nedeljivem koraku. Temu pravimo atomarna operacija. Da z orodjem OpenMP neko enostavno operacijo izpeljemo kod nedeljivo celoto, uporabimo ukaz #pragma omp atomic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <omp.h>

int main (int argc, char *argv[])
{
  int nt; sscanf (argv[1], "%d", &nt);

  int sum = 0;
  #pragma omp parallel num_threads(nt)
  {
    int n = omp_get_thread_num ();
    #pragma omp atomic
      sum = sum + n;
    printf ("%d: %d\n", n, sum);
  }
  printf ("\n%d\n", sum);
  return 0;
}

Zaradi ukaza #pragma omp atomic vsaka nit programa synchro-atomic-1.c

  1. prebere spremenljivko sum iz pomnilnika,
  2. ji prišteje vrednost spremenljivke n in
  3. spremenljivko sum zapiše nazaj v pomnilnik

ne da bi katerakoli druga nit imela dostop do spremenljivke sum. Zato program synchro-atomic-1.c pravilno izračuna vsoto oznak niti.

Vaja

Kljub temu, da program synchro-atomic-1.c pravilno izračuna vsoto oznak niti, večina niti še vedno izpiše napačno vsoto. Zakaj?

Atomarne operacije so lahko podane le kot stavki, ki so razmeroma enostavni izrazi in prirejanja. V ukazu #pragma omp atomic lahko določimo, za kakšen atomaren dostop do spremenljivke gre:

  • določilo read omogoči atomarno branje spremenljivke x v stavku expr=x;
  • določilo write omogoči atomarno pisanje spremenljivke x v stavku x=expr;
  • določilo update omogoči atomarni branje-in-pisanje spremenljivke x v stavku x=x+expr.

Kritična sekcija

Drug način reševanja problema, ko več niti hkrati izvaja operacijo nad skupno spremenljivko, je uporaba kritične sekcije. To je del niti, za katerega mora veljati, da ga v vsakem trenutku izvaja kvečjemu ena sama nit. Problem seštevanja oznak niti z uporabo kritične sekcije rešimo precej podobno kot z uporabo atomarne operacije. Tokrat uporabimo ukaz #pragma omp critical:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <omp.h>

int main (int argc, char *argv[])
{
  int nt; sscanf (argv[1], "%d", &nt);

  int sum = 0;
  #pragma omp parallel num_threads(nt)
  {
    int n = omp_get_thread_num ();
    #pragma omp critical (adding_thread_numbers)
      sum = sum + n;
    printf ("%d: %d\n", n, sum);
  }
  printf ("%d\n", sum);
  return 0;
}

Čeprav je na prvi pogled rešitev precej podobna, omogoča kritična sekcija izvajanje ne le enostavnih izrazov in prirejanj, pač pa povsem poljubne kode, če je ta koda lepo zaprta v blok. Predvsem pa lahko kritični sekciji damo ime (v programu synchro-critical-1.c je to adding_thread_numbers), kar omogoča, da se ista kritična sekcija pojavlja večkrat v programu (ime kritične sekcije je celo vidno zunaj prevajalne enote).

Vaja

Kljub temu, da program synchro-critical-1.c pravilno izračuna vsoto oznak niti, večina niti še vedno izpiše napačno vsoto. Zakaj?

Vaja

V program synchro-critical-1.c dodajte števec prištevanj spremenljivki sum. Obe operaciji, prištevanje spremenljivki sum in povečevanje števca prištevanj opravite v isti kritični sekciji, ob enem pa vsakič še izpišite vrednost števca.

Sinhronizacijska pregrada

V določenih primerih se morajo niti posamezne skupine na določenem mestu sinhronizirati: tiste niti, ki so svoje računanje to tega mesta opravile hitreje, morajo počakati vse ostale, da prirejo do tega mesta v programu. Na ta način pogosto rešujemo probleme z zagotavljanjem konsistentnosti podatkov v skupnem pomnilniku in vhodno-izhodnimi operacijami. Na mesto v programu, kjer naj se niti sinhronizirajo, postavimo sinhronizacijsko pregrado. To naredimo z ukazom #parallel omp barrier.

V programu synchro-atomic-1.c mnoge niti takoj potem, ko prištejejo svojo oznako skupni vsoti, izpišejo vrednost spremenljivke sum ne da bi počakale, da to isto storijo tudi ostale niti. Da tak predčasen izpis onemogočimo, pred izpis postavimo sinhronizacijsko pregrado: šele ko jo bodo dosegle vse niti in bodo torej vse oznake prištete skupni vsoti, bo nitim omogočen prehod preko sinhronizacijske pregrade in s tem izpis sprmenljivke sum znotrej niti. To je prikazano s programom synchro-atomic-barrier-1.c.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <omp.h>

int main (int argc, char *argv[])
{
  int nt; sscanf (argv[1], "%d", &nt);

  int sum = 0;
  #pragma omp parallel num_threads(nt)
  {
    int n = omp_get_thread_num ();
    #pragma omp atomic
      sum = sum + n;
    #pragma omp barrier
    printf ("%d: %d\n", n, sum);
  }
  printf ("\n%d\n", sum);
  return 0;
}

Vaja

Dopolnite še program synchro-critical-1.c, da bo vsaka nit izpisala pravilno vsoto oznak niti.

Na tem mestu povejmo še to, da je na koncu vsakega nitenja, torej na koncu dela kode, ki je označen z ukazom #pragma omp parallel, vedno postavljena sinhronizacijska pregrada.

Omejitev računanja na eno nit

Ne tem mestu omenimo še dva dodatna ukaza orodja OpenMP: #pragma omp master in #pragma omp single. Oba povzročita, da se koda, ki je označena z vsakim od teh dveh ukazov, izvrši le v eni sami niti, v ostalih nitih skupine pa ne:

  • koda označena z ukazom #pragma omp master se izvrši le v glavni niti, ne pri vstopu ne pri izstopu iz označene kode pa ni nobene sinhronizacijske pregrade;
  • koda označena z ukazom #pragma omp single se izvrši le v eni naključno izbrani niti, pri izstopu pa je postavljena sinhronizacijska pregrada (ki jo je moč umakniti z dopolnilom nowait).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <omp.h>

int main (int argc, char *argv[])
{
  int nt; sscanf (argv[1], "%d", &nt);

  int sum = 0;
  #pragma omp parallel num_threads(nt)
  {
    int n = omp_get_thread_num ();
    #pragma omp master
      printf ("master(%d): start\n", n);
    #pragma omp atomic
      sum = sum + n;
    #pragma omp master
      printf ("master(%d): stop\n", n);
    printf ("%d: %d\n", n, sum);
  }
  printf ("\n%d\n", sum);
  return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <omp.h>

int main (int argc, char *argv[])
{
  int nt; sscanf (argv[1], "%d", &nt);

  int sum = 0;
  #pragma omp parallel num_threads(nt)
  {
    int n = omp_get_thread_num ();
    #pragma omp single
      printf ("single(%d): start\n", n);
    #pragma omp atomic
      sum = sum + n;
    #pragma omp single nowait
      printf ("single(%d): stop\n", n);
    printf ("%d: %d\n", n, sum);
  }
  printf ("\n%d\n", sum);
  return 0;
}

Vaja

Vsaka nit programa single-1.c pravilno izpiše vsoto oznak niti. Zakaj?
Niti programa master-1.c ne izpišejo vsote oznak niti pravilno. Zakaj?
Dopolnite program "master-1.c" tako, da bodo vse niti pravilno izpisale vsoto oznak niti.

Vaja

Spremenite programa master-1.c in single-1.c tako, da delujeta enako, a pri tem nista uporabljena ukaza #pragma omp master in #pragma omp single.