Programmēšana

Izvairieties no sinhronizācijas strupceļiem

Manā iepriekšējā rakstā "Double-Checked Locking: Clever, but Broken" (JavaWorld, 2001. gada februārī), es aprakstīju, kā vairākas izplatītas metodes, kā izvairīties no sinhronizācijas, faktiski ir nedrošas, un ieteica stratēģiju “Ja rodas šaubas, sinhronizējiet”. Parasti jums vajadzētu sinhronizēt ikreiz, kad lasāt jebkuru mainīgo, kuru, iespējams, iepriekš ir uzrakstījis cits pavediens, vai ikreiz, kad rakstāt jebkuru mainīgo, kuru vēlāk var nolasīt cits pavediens. Turklāt, lai gan sinhronizācijai tiek piemērots izpildes sods, sods, kas saistīts ar nepārtrauktu sinhronizāciju, nav tik liels, kā ieteica daži avoti, un tas ir vienmērīgi samazinājies ar katru nākamo JVM ieviešanu. Tāpēc šķiet, ka tagad ir mazāk iemeslu nekā jebkad agrāk izvairīties no sinhronizācijas. Tomēr vēl viens risks ir saistīts ar pārmērīgu sinhronizāciju: strupceļš.

Kas ir strupceļš?

Mēs sakām, ka procesu vai pavedienu kopa ir strupceļā kad katrs pavediens gaida notikumu, ko var izraisīt tikai cits process kopā. Vēl viens veids, kā ilustrēt strupceļu, ir izveidot vērstu grafu, kura virsotnes ir pavedieni vai procesi un kura malas attēlo relāciju "gaida". Ja šajā diagrammā ir cikls, sistēma ir strupceļā. Ja vien sistēma nav paredzēta, lai atgūtu no strupceļiem, strupceļa dēļ programma vai sistēma pakārt.

Sinhronizācijas strupceļi Java programmās

Strupceļi var rasties Java, jo sinhronizēts atslēgvārds liek izpildes pavedienam bloķēties, gaidot bloķēšanu vai monitoru, kas saistīts ar norādīto objektu. Tā kā pavedienā jau var būt slēdzenes, kas saistītas ar citiem objektiem, divi pavedieni katrs varētu gaidīt, kamēr otrs atbrīvos slēdzeni; šādā gadījumā viņi beigs gaidīt mūžīgi. Šajā piemērā parādīts metožu kopums, kas var nonākt strupceļā. Abas metodes iegūst slēdzenes diviem bloķēšanas objektiem, cacheLock un tableLock, pirms viņi turpina. Šajā piemērā objekti, kas darbojas kā slēdzenes, ir globāli (statiski) mainīgie, kas ir izplatīta metode lietojumprogrammu bloķēšanas vienkāršošanai, veicot bloķēšanu rupjākajā detalizācijas pakāpē:

Saraksts 1. Potenciāls sinhronizācijas strupceļš

 public static Object cacheLock = new Object (); public static Object tableLock = new Object (); ... public void oneMethod () {synchronized (cacheLock) {synchronized (tableLock) {doSomething (); }}} public void anotherMethod () {synchronized (tableLock) {synchronized (cacheLock) {doSomethingElse (); }}} 

Tagad iedomājieties, ka A pavediens izsauc oneMethod () kamēr B pavediens vienlaikus zvana citsMetode (). Iedomājieties tālāk, ka pavediens A iegūst slēdzeni cacheLock, un tajā pašā laikā vītne B iegūst slēdzeni tableLock. Tagad pavedieni ir strupceļā: neviens no pavedieniem neatdos savu slēdzeni, kamēr tas neiegūs otru slēdzeni, bet neviens no tiem nevarēs iegūt otru slēdzeni, kamēr otrs pavediens no tā neatsakās. Kad Java programma strupceļā, strupceļā esošie pavedieni vienkārši gaida mūžīgi. Kaut arī citi pavedieni var turpināt darboties, jums galu galā būs jānogalina programma, jārestartē un jācer, ka tā vairs nav strupceļā.

Testēt strupceļus ir grūti, jo strupceļi ir atkarīgi no laika, slodzes un vides, un tādējādi tie var notikt reti vai tikai noteiktos apstākļos. Kodam var būt strupceļš, piemēram, 1. saraksts, taču tas neuzrāda strupceļu, līdz rodas kāda nejaušu un nejaušu notikumu kombinācija, piemēram, programma tiek pakļauta noteiktam slodzes līmenim, darbojas ar noteiktu aparatūras konfigurāciju vai tiek pakļauta noteiktai iedarbībai. lietotāju darbību un vides apstākļu kopums. Strupceļš atgādina bumbas ar laika degli, kas gaida eksplodēšanu mūsu kodā; kad viņi to dara, mūsu programmas vienkārši karājas.

Nekonsekventa slēdzenes pasūtīšana izraisa strupceļus

Par laimi, mēs varam uzlikt salīdzinoši vienkāršu prasību slēdzenes iegūšanai, kas var novērst sinhronizācijas strupceļus. 1. saraksta metožu iespējamība ir strupceļš, jo katra metode iegūst divas slēdzenes citā secībā. Ja 1. saraksts būtu rakstīts tā, ka katra metode ieguva abas slēdzenes vienā secībā, divi vai vairāki pavedieni, kas izpilda šīs metodes, nevarēja nonākt strupceļā neatkarīgi no laika vai citiem ārējiem faktoriem, jo ​​neviens pavediens nevarēja iegūt otro slēdzeni, jau neturot vispirms. Ja jūs varat garantēt, ka slēdzenes vienmēr tiks iegūtas konsekventā secībā, jūsu programma nebūs strupceļā.

Strupceļš ne vienmēr ir tik acīmredzams

Kad esat pielāgojies slēdzenes pasūtīšanas nozīmei, jūs varat viegli atpazīt 1. saraksta problēmu. Tomēr līdzīgas problēmas var izrādīties mazāk acīmredzamas: varbūt abas metodes atrodas atsevišķās klasēs vai varbūt iesaistītās slēdzenes tiek iegūtas netieši, izsaucot sinhronizētas metodes, nevis tieši izmantojot sinhronizētu bloku. Apsveriet šīs divas sadarbības klases, Modelis un Skats, vienkāršotā MVC (Model-View-Controller) sistēmā:

Saraksts 2. Smalkāka potenciālās sinhronizācijas strupceļš

 publiskās klases modelis {private View myView; publiski sinhronizēts void updateModel (Object someArg) {doSomething (someArg); myView.somethingChanged (); } publiski sinhronizēts objekts getSomething () {return someMethod (); }} publiskā klase Skatīt {privātais modelis pamatāModel; publiski sinhronizēts void somethingChanged () {doSomething (); } public synchronized void updateView () {Object o = myModel.getSomething (); }} 

2. sarakstā ir divi objekti, kas sadarbojas un kuriem ir sinhronizētas metodes; katrs objekts izsauc otra sinhronizētās metodes. Šī situācija atgādina 1. sarakstu - divas metodes iegūst slēdzenes uz tiem pašiem diviem objektiem, bet dažādās secībās. Tomēr nekonsekventā bloķēšanas kārtība šajā piemērā ir daudz mazāk acīmredzama nekā 1. sarakstā, jo bloķēšanas iegūšana ir netieša metodes izsaukuma sastāvdaļa. Ja zvana viens pavediens Model.updateModel () kamēr cits pavediens vienlaikus zvana View.updateView (), pirmais pavediens varētu iegūt Modelisslēdzeni un gaidiet Skatsslēdzeni, bet otrs iegūst Skatsslēdzeni un mūžīgi gaida Modelisslēdzene.

Sinhronizācijas strupceļa potenciālu var aprakt vēl dziļāk. Apsveriet šo piemēru: jums ir metode naudas pārskaitīšanai no viena konta uz citu. Pirms pārsūtīšanas vēlaties iegūt slēdzenes abos kontos, lai pārliecinātos, ka pārsūtīšana ir atomu. Apsveriet šo nekaitīgā izskata ieviešanu:

Uzskaitīšana 3. Vēl smalkāka potenciālās sinhronizācijas strupceļš

 public void transferMoney (Konts noAccount, Konts uzAccount, DollarAmount summaToTransfer) {synchronized (fromAccount) {synchronized (toAccount) {if (fromAccount.hasSufficientBalance (summaToTransfer) {fromAccount.debit (summaToTransfer}); } 

Pat ja visas metodes, kas darbojas divos vai vairākos kontos, izmanto vienu un to pašu secību, 3. sarakstā ir tās pašas strupceļa problēmas sēklas kā 1. un 2. sarakstā, taču vienmērīgākā veidā. Apsveriet, kas notiek, kad pavediens A izpilda:

 transferMoney (accountOne, accountTwo, summa); 

Tajā pašā laikā pavediens B izpilda:

 transferMoney (kontsTwo, accountOne, citsAmount); 

Atkal abi pavedieni mēģina iegūt vienas un tās pašas divas slēdzenes, bet dažādās kārtās; strupceļa risks joprojām pastāv, bet daudz mazāk acīmredzamā formā.

Kā izvairīties no strupceļiem

Viens no labākajiem veidiem, kā novērst strupceļa iespējamību, ir izvairīties no vairāk nekā vienas slēdzenes iegūšanas vienlaikus, kas bieži vien ir praktiski. Tomēr, ja tas nav iespējams, jums ir nepieciešama stratēģija, kas nodrošina vairāku slēdzeņu iegūšanu konsekventā, noteiktā secībā.

Atkarībā no tā, kā jūsu programma izmanto slēdzenes, var nebūt sarežģīti nodrošināt konsekventu bloķēšanas kārtību. Dažās programmās, piemēram, 1. sarakstā, visas kritiskās slēdzenes, kas varētu piedalīties vairāku bloķēšanā, tiek iegūtas no neliela atsevišķu bloķēšanas objektu komplekta. Tādā gadījumā jūs varat definēt slēdzenes iegūšanas secību slēdzeņu komplektā un pārliecināties, ka jūs vienmēr iegādājaties slēdzenes šajā secībā. Kad bloķēšanas secība ir definēta, tā vienkārši ir labi jādokumentē, lai veicinātu konsekventu izmantošanu visā programmā.

Samaziniet sinhronizētos blokus, lai izvairītos no vairākkārtējas bloķēšanas

2. sarakstā problēma kļūst sarežģītāka, jo sinhronizētas metodes izsaukšanas rezultātā slēdzenes tiek iegūtas netieši. Parasti jūs varat izvairīties no iespējamām strupceļām, kas rodas no tādiem gadījumiem kā 2. saraksts, sašaurinot sinhronizācijas darbības jomu līdz pēc iespējas mazākam blokam. Vai Model.updateModel () tiešām vajag turēt Modelis bloķēt, kamēr tas zvana View.somethingChanged ()? Bieži vien tā nav; visa metode, visticamāk, tika sinhronizēta kā saīsne, nevis tāpēc, ka vajadzēja sinhronizēt visu metodi. Tomēr, ja jūs nomaināt sinhronizētās metodes ar mazākiem sinhronizētiem blokiem metodes iekšpusē, šī bloķēšanas darbība ir jādokumentē kā daļa no metodes Javadoc. Zvanītājiem jāzina, ka viņi var droši izsaukt metodi bez ārējas sinhronizācijas. Zvanītājiem būtu jāzina arī metodes bloķēšanas darbība, lai viņi varētu pārliecināties, ka slēdzenes tiek iegūtas konsekventā secībā.

Sarežģītāka atslēgu pasūtīšanas tehnika

Citās situācijās, piemēram, 3. saraksta bankas konta piemērs, fiksēta pasūtījuma kārtulas piemērošana kļūst vēl sarežģītāka; jums jānosaka kopējais pasūtījums objektu kopai, kas ir piemērota bloķēšanai, un jāizmanto šī secība, lai izvēlētos bloķēšanas iegūšanas secību. Tas izklausās netīrs, bet patiesībā ir vienkārši. 4. saraksts ilustrē šo tehniku; lai izmantotu pasūtījumu, tas izmanto skaitlisku konta numuru Konts objektiem. (Ja bloķējamajam objektam trūkst dabiskās identitātes rekvizīta, piemēram, konta numura, varat izmantot Object.identityHashCode () metodi tā vietā.)

Saraksts 4. Izmantojiet pasūtījumu, lai iegūtu slēdzenes fiksētā secībā

 public void transferMoney (konts noAccount, konts uz kontu, DollarAmount summaToTransfer) {konts firstLock, secondLock; if (fromAccount.accountNumber () == toAccount.accountNumber ()) mest jaunu izņēmumu ("Nevar pārsūtīt no konta uz sevi"); else if (fromAccount.accountNumber () <toAccount.accountNumber ()) {firstLock = noAccount; secondLock = uz kontu } else {firstLock = toAccount; secondLock = noKonta; } synchronized (firstLock) {synchronized (secondLock) {if (fromAccount.hasSufficientBalance (summaToTransfer) {fromAccount.debit (summaToTransfer); uzAccount.credit (summaToTransfer);}}}} 

Tagad secība, kādā konti tiek norādīti zvanā uz pārskaitīt naudu() nav nozīmes; slēdzenes vienmēr tiek iegūtas vienā secībā.

Vissvarīgākā daļa: dokumentācija

Jebkuras bloķēšanas stratēģijas kritisks elements, bet bieži tiek ignorēts, ir dokumentācija. Diemžēl pat gadījumos, kad bloķēšanas stratēģijas izstrādei tiek pievērsta īpaša uzmanība, bieži vien tiek dokumentēti daudz mazāk pūļu. Ja jūsu programmā tiek izmantots neliels atsevišķu slēdzeņu kopums, jums pēc iespējas skaidrāk jādokumentē pieņēmumi par slēdzenes pasūtīšanu, lai nākamie uzturētāji varētu izpildīt slēdzenes pasūtīšanas prasības. Ja metodei jāiegūst slēdzene, lai tā varētu veikt savu funkciju, vai arī tā jāsauc, turot noteiktu slēdzeni, metodes faktiem jāņem vērā šis fakts. Tādā veidā nākotnes izstrādātāji zinās, ka noteiktas metodes izsaukšana var izraisīt slēdzenes iegūšanu.

Dažas programmas vai klases bibliotēkas atbilstoši dokumentē to bloķēšanas izmantošanu. Katrai metodei vajadzētu dokumentēt vismaz tās iegūtās slēdzenes un to, vai zvanītājiem ir jābūt slēdzenei, lai metodi droši izsauktu. Turklāt klasēm vajadzētu dokumentēt, vai tās ir vai nav drošas, vai ar kādiem nosacījumiem.

Koncentrējieties uz bloķēšanas uzvedību projektēšanas laikā

Tā kā strupceļš bieži nav acīmredzams un notiek reti un neparedzami, tie var radīt nopietnas problēmas Java programmās. Pievēršot uzmanību programmas bloķēšanas uzvedībai projektēšanas laikā un definējot noteikumus, kad un kā iegūt vairākas slēdzenes, jūs varat ievērojami samazināt strupceļu iespējamību. Neaizmirstiet rūpīgi dokumentēt savas programmas bloķēšanas iegūšanas noteikumus un sinhronizācijas izmantošanu; laiks, kas pavadīts vienkāršu bloķēšanas pieņēmumu dokumentēšanai, atmaksāsies, ievērojami samazinot strupceļa un citu vienlaicīguma problēmu iespējamību vēlāk.

Braiens Gecs ir profesionāls programmatūras izstrādātājs ar vairāk nekā 15 gadu pieredzi. Viņš ir galvenais konsultants programmatūras izstrādes un konsultāciju uzņēmumā Quiotix, kas atrodas Los Altos, Kalifornijā.
$config[zx-auto] not found$config[zx-overlay] not found