Strupceļi un bloķēšanas bloki - kā reālajā pasaulē izvairīties no vienlaicīguma?

Noklūšanās var notikt tikai vienlaicīgās (vairāku vītņu) programmās, kurās pavedieni sinhronizē (izmanto slēdzenes) piekļuvi vienam vai vairākiem koplietotiem resursiem (mainīgajiem un objektam) vai instrukciju kopai (kritiskā sadaļa).

Tiešās bloķēšanas rodas, mēģinot izvairīties no strupceļiem, izmantojot asinhrono bloķēšanu, kur vairāki pavedieni konkurē par vienu un to pašu komplektu (s), izvairoties no slēdzenes (-ņu) iegūšanas, lai citi pavedieni varētu iet vispirms ar slēdzeni, un galu galā nekad neiegūstos bloķēt un turpināt; izraisot badu. Skatiet zemāk, lai saprastu, kā aysnc bloķēšana, kas ir stratēģija, lai izvairītos no strupceļa, var būt Livelock iemesls

Šeit ir daži no strupceļu teorētiskajiem risinājumiem, un viens no tiem (otrais) ir Livelocks galvenais iemesls

Teorētiskās pieejas

Nelietojiet slēdzenes

Nav iespējams, ja ir jāsinhronizē divas operācijas, piemēram, vienkāršs bankas pārskaitījums, kad no viena konta tiek debetēts, pirms jūs varat kreditēt otru kontu, un neļaujiet nevienam citam pavedienam pieskarties kontu atlikumam, kamēr pašreizējais pavediens nav veikts.

Nebloķējiet slēdzenes, ja pavediens nevar iegūt slēdzeni, tam vajadzētu atbrīvot iepriekš iegūtos slēdzenes, lai vēlāk mēģinātu vēlreiz

Apgrūtinoša ieviešana un var izraisīt badu (Livelocks), ja pavediens vienmēr ļauj slēdzenēm iet tikai mēģināt vēlreiz un darīt to pašu. Turklāt šai pieejai var būt pārlieku lielas biežās pavedienu konteksta maiņas, samazinot sistēmas kopējo veiktspēju. Arī CPU plānotājam nav taisnīguma, jo viņš nezina, kurš pavediens visilgāk ir gaidījis bloķēšanu (-as).

Ļaujiet pavedieniem vienmēr pieprasīt slēdzenes, stingri pasūtot

Piemēram, vieglāk pateikt, nekā izdarīt. Ja mēs rakstām funkciju naudas pārskaitīšanai no konta A uz B, mēs varam uzrakstīt kaut ko līdzīgu

// sastādīšanas laikā mēs bloķējam pirmo arg, pēc tam otro
publisks nederīgs pārskaitījums (konts A, konts B, liela nauda) {
  sinhronizēts (A) {
    sinhronizēts (B) {
      A.add (summa);
      B.aparaksts (summa);
    }
  }
}
// izpildlaikā mēs nevaram izsekot, kā tiks sauktas mūsu metodes
public void run () {
  jauns pavediens (() -> this.transfer (X, Y, 10000)) start ();
  jauns pavediens (() -> this.transfer (Y, X, 10000)) start ();
}
// šī darbība () radīs strupceļu
// pirmā vītne nofiksējas uz X, gaida Y
// otrais pavediens aizslēdzas uz Y, gaida X

Reālās pasaules risinājums

Mēs varam apvienot slēdzeņu pasūtīšanas un laika slēdzeņu pieejas, lai iegūtu reālu vārdu risinājumu

Biznesa noteikts slēdzeņu pasūtīšana

Mēs varam uzlabot savu pieeju, diskriminējot A un B, pamatojoties uz to, kura konta numurs ir lielāks vai mazāks.

// izpildes laikā vispirms tiek ņemts vērā konts ar mazāku ID
publisks nederīgs pārskaitījums (konts A, konts B, liela nauda) {
  galīgais konts vispirms = A.id 
  sinhronizēts (pirmais) {
    sinhronizēts (otrais) {
      first.add (summa);
      otrais.sadalījums (summa);
    }
  }
}
// izpildlaikā mēs nevaram izsekot, kā tiks sauktas mūsu metodes
public void run () {
  jauns pavediens (() -> this.transfer (X, Y, 10000)) start ();
  jauns pavediens (() -> this.transfer (Y, X, 10000)) start ();
}

Piemēram, ja X.id = 1111 un Y.id = 2222, jo kā pirmo kontu mēs uzskatām kontu ar mazāku konta ID, pārsūtīšanas (Y, X, 10000) un pārskaitījuma (X, Y, 10000) būs tāds pats. Ja X konta numurs ir mazāks par Y, abi pavedieni mēģinās bloķēt X pirms Y, un tikai vienam no tiem veiksies un tiks bloķēta Y apdare un atbrīvotas slēdzenes X un Y, pirms citi pavedieni iegūs slēdzenes un varēs turpināt.

Biznesa noteiktā laikā nogaidiet tryLock / async bloķēšanas pieprasījumus

Uzņēmējdarbības noteiktu bloķēšanas pasūtīšanas risinājums darbojas tikai tad, ja tiek izmantotas asociatīvas attiecības, kur loģika vienā vietā pārsūta (….), Piemēram, mūsu metodē, nosaka, kā resursi tiek koordinēti.

Mums var būt citas metodes / loģika, kas beidzas ar pasūtīšanas loģikas izmantošanu, kas nav savienojama ar pārsūtīšanu (…). Lai šādos gadījumos izvairītos no strupceļa, ieteicams izmantot async bloķēšanu, kur mēs cenšamies bloķēt resursus ierobežotam / reālistiskam laikam (maksimālais transakcijas laiks) + mazam-nejaušam gaidīšanas laikam, lai visi pavedieni nemēģinātu vēlreiz iegādājieties pārāk agri un attiecīgi ne visi vienlaikus, tādējādi izvairoties no Livelocks (bada dēļ neatgriezeniskiem mēģinājumiem iegūt slēdzenes)

// pieņemsim, ka konts # getLock () dod mums konta atslēgu (java.util.concurrent.locks.Lock)
// Konts varētu iekapsulēt slēdzeni, nodrošināt slēdzeni () / atbloķēt ()
public long getWait () {
/// atgriež pēdējās n pārsūtīšanas reizes mainīgo vidējo pārsūtīšanas laiku + mazs-nejaušs sāls milos, lai visi pavedieni, kas gaida bloķēšanu, neatmostos vienlaikus.
}
publisks spēkā neesošs pārskaitījums (Lock lockF, Lock lockS, int summa) {
  galīgais konts vispirms = A.id 
  Būla darīts = nepatiess;
  darīt {
    izmēģiniet {
      izmēģiniet {
        if (lockF.tryLock (getWait (), MILLISECONDS)) {
          izmēģiniet {
            if (lockS.tryLock (getWait (), MILLISECONDS)) {
              darīts = patiess;
            }
          } beidzot {
            lockS.unlock ();
          }
        }
      } nozveja (InterruptedException e) {
        mest jauno RuntimeException (“Atcelts”);
      }
    } beidzot {
      lockF.unlock ();
    }
  } kamēr (! darīts);

}
// izpildlaikā mēs nevaram izsekot, kā tiks sauktas mūsu metodes
public void run () {
    jauns pavediens (() -> this.transfer (X, Y, 10000)) start ();
    jauns pavediens (() -> this.transfer (Y, X, 10000)) start ();
}