Testing av flertrådet kode i Java

1. Introduksjon

I denne opplæringen vil vi dekke noen av det grunnleggende om å teste et samtidig program. Vi vil først og fremst fokusere på trådbasert samtidighet og problemene den gir i testing.

Vi vil også forstå hvordan vi kan løse noen av disse problemene og teste flertrådet kode effektivt i Java.

2. Samtidig programmering

Samtidig programmering refererer til programmering der vi bryte ned et stort stykke beregning i mindre, relativt uavhengige beregninger.

Hensikten med denne øvelsen er å kjøre disse mindre beregningene samtidig, muligens til og med parallelt. Selv om det er flere måter å oppnå dette på, er målet alltid å kjøre programmet raskere.

2.1. Tråder og samtidig programmering

Med prosessorer som pakker flere kjerner enn noensinne, er samtidig programmering i forkant for å utnytte dem effektivt. Imidlertid forblir faktum det samtidige programmer er mye vanskeligere å designe, skrive, teste og vedlikeholde. Så hvis vi tross alt kan skrive effektive og automatiserte testtilfeller for samtidige programmer, kan vi løse en stor del av disse problemene.

Så hva gjør det å skrive tester for samtidig kode så vanskelig? For å forstå det, må vi forstå hvordan vi oppnår samtidighet i programmene våre. En av de mest populære samtidige programmeringsteknikkene innebærer bruk av tråder.

Nå kan tråder være innfødte, i så fall er de planlagt av de underliggende operativsystemene. Vi kan også bruke det som kalles grønne tråder, som er planlagt av en kjøretid direkte.

2.2. Vanskeligheter med å teste samtidige programmer

Uavhengig av hvilken type tråder vi bruker, er det som gjør dem vanskelige å bruke, trådkommunikasjon. Hvis vi virkelig klarer å skrive et program som involverer tråder, men ingen trådkommunikasjon, er det ikke noe bedre! Mer realistisk vil tråder vanligvis måtte kommunisere. Det er to måter å oppnå dette på - delt minne og overføring av meldinger.

Hovedtyngden av problemet forbundet med samtidig programmering oppstår ved å bruke innfødte tråder med delt minne. Det er vanskelig å teste slike programmer av samme grunner. Flere tråder med tilgang til delt minne krever vanligvis gjensidig utelukkelse. Vi oppnår dette vanligvis gjennom en beskyttelsesmekanisme ved bruk av låser.

Men dette kan fremdeles føre til en rekke problemer som løpsforhold, live låser, fastlåsning og sult, for å nevne noen. Dessuten er disse problemene intermitterende, da trådplanlegging i tilfelle av innfødte tråder er helt ikke-deterministisk.

Derfor er det virkelig en utfordring å skrive effektive tester for samtidige programmer som kan oppdage disse problemene på en deterministisk måte!

2.3. Anatomy of Thread Interleaving

Vi vet at innfødte tråder kan planlegges av operativsystemer uforutsigbart. I tilfelle disse tråder får tilgang til og endrer delte data, gir det opphav til interessant trådsammenfletting. Selv om noen av disse sammenflettinger kan være helt akseptable, kan andre la de endelige dataene være i en uønsket tilstand.

La oss ta et eksempel. Anta at vi har en global teller som økes av hver tråd. Ved slutten av behandlingen vil vi at tilstanden til denne telleren skal være nøyaktig den samme som antall tråder som er utført:

privat int counter; offentlig ugyldig økning () {counter ++; }

Nå, til inkrement et primitivt heltall i Java er ikke en atomoperasjon. Den består i å lese verdien, øke den og til slutt lagre den. Mens flere tråder gjør den samme operasjonen, kan det gi mange mulige sammenflettinger:

Mens denne spesielle sammenflettingen gir helt akseptable resultater, hva med denne:

Dette var ikke det vi forventet. Tenk deg hundrevis av tråder som kjører kode som er mye mer komplisert enn dette. Dette vil gi opphav til ufattelige måter som trådene vil krysse sammen.

Det er flere måter å skrive kode som unngår dette problemet, men det er ikke temaet for denne opplæringen. Synkronisering ved hjelp av en lås er en av de vanligste, men den har problemer knyttet til løpsforhold.

3. Testing av kode med flere tråder

Nå som vi forstår de grunnleggende utfordringene ved å teste kode med flere tråder, vil vi se hvordan vi kan overvinne dem. Vi bygger en enkel brukssak og prøver å simulere så mange problemer knyttet til samtidighet som mulig.

La oss begynne med å definere en enkel klasse som teller muligens hva som helst:

offentlig klasse MyCounter {private int count; offentlig tomrumsøkning () {int temp = count; count = temp + 1; } // Getter for telling}

Dette er en tilsynelatende harmløs kode, men det er ikke vanskelig å forstå at det ikke er trådsikkert. Hvis vi tilfeldigvis skriver et program sammen med denne klassen, vil det være feil. Formålet med testing her er å identifisere slike feil.

3.1. Testing av ikke-samtidige deler

Som en tommelregel, Det er alltid tilrådelig å teste koden ved å isolere den fra enhver samtidig oppførsel. Dette for å med rimelighet sikre at det ikke er noen annen feil i koden som ikke er relatert til samtidighet. La oss se hvordan vi kan gjøre det:

@Test public void testCounter () {MyCounter counter = new MyCounter (); for (int i = 0; i <500; i ++) {counter.increment (); } assertEquals (500, counter.getCount ()); }

Selv om det ikke er mye som skjer her, gir denne testen oss tillit til at den fungerer i det minste i fravær av samtidighet.

3.2. Første forsøk på å teste med samtidighet

La oss gå videre for å teste den samme koden igjen, denne gangen i et samtidig oppsett. Vi prøver å få tilgang til samme forekomst av denne klassen med flere tråder og se hvordan den oppfører seg:

@Test offentlig ugyldig testCounterWithConcurrency () kaster InterruptedException {int numberOfThreads = 10; ExecutorService-tjeneste = Executors.newFixedThreadPool (10); CountDownLatch latch = ny CountDownLatch (numberOfThreads); MyCounter-teller = ny MyCounter (); for (int i = 0; i {counter.increment (); latch.countDown ();}); } latch.await (); assertEquals (numberOfThreads, counter.getCount ()); }

Denne testen er rimelig, ettersom vi prøver å operere på delte data med flere tråder. Når vi holder antallet tråder lavt, som 10, vil vi merke at det går nesten hele tiden. Interessant, hvis vi begynner å øke antall tråder, si til 100, vil vi se at testen begynner å mislykkes mesteparten av tiden.

3.3. Et bedre forsøk på å teste med samtidighet

Mens den forrige testen avslørte at koden vår ikke er trådsikker, er det et problem med denne spenen. Denne testen er ikke deterministisk fordi de underliggende trådene fletter sammen på en ikke-deterministisk måte. Vi kan virkelig ikke stole på denne testen for programmet vårt.

Det vi trenger er en måte å kontrollere sammenflettingen av tråder slik at vi kan avsløre problemer med samtidig på en deterministisk måte med mye færre tråder. Vi begynner med å tilpasse koden vi tester litt:

offentlig synkronisert tomromstigning () kaster InterruptedException {int temp = count; vent (100); count = temp + 1; }

Her har vi laget metoden synkronisert og introduserte en ventetid mellom de to trinnene i metoden. De synkronisert nøkkelordet sørger for at bare en tråd kan endre telle variabel om gangen, og ventetiden introduserer en forsinkelse mellom hver trådutførelse.

Vær oppmerksom på at vi ikke nødvendigvis trenger å endre koden vi har tenkt å teste. Men siden det ikke er mange måter vi kan påvirke trådplanlegging på, tyr vi til dette.

I en senere del vil vi se hvordan vi kan gjøre dette uten å endre koden.

La oss nå teste denne koden på samme måte som vi gjorde tidligere:

@Test offentlig ugyldig testSummationWithConcurrency () kaster InterruptedException {int numberOfThreads = 2; ExecutorService-tjeneste = Executors.newFixedThreadPool (10); CountDownLatch latch = ny CountDownLatch (numberOfThreads); MyCounter-teller = ny MyCounter (); for (int i = 0; i {try {counter.increment ();} catch (InterruptedException e) {// Handle exception} latch.countDown ();}); } latch.await (); assertEquals (numberOfThreads, counter.getCount ()); }

Her kjører vi dette bare med to tråder, og sjansen er at vi vil kunne få den mangelen vi har savnet. Det vi har gjort her er å prøve å oppnå en spesifikk trådsammenfletting, som vi vet kan påvirke oss. Selv om det er bra for demonstrasjonen, kanskje vi ikke finner dette nyttig for praktiske formål.

4. Testverktøy tilgjengelig

Etter hvert som antall tråder vokser, vokser det mulige antall måter de kan flettes sammen eksponentielt på. Det er bare ikke mulig å finne ut alle slike sammenflettinger og teste for dem. Vi må stole på verktøy for å gjøre den samme eller lignende innsatsen for oss. Heldigvis er det noen av dem som er tilgjengelige for å gjøre livet vårt enklere.

Det er to brede kategorier verktøy tilgjengelig for oss for testing av samtidig kode. Den første gjør det mulig for oss å produsere rimelig høyt stress på den samtidige koden med mange tråder. Stress øker sannsynligheten for sjelden sammenfletting, og øker dermed sjansene våre for å finne feil.

Det andre gjør det mulig for oss å simulere spesifikk trådinnfletting, og hjelper oss med å finne feil med større sikkerhet.

4.1. tempus-fugit

Tempus-fugit Java-biblioteket hjelper oss med å skrive og teste samtidig kode enkelt. Vi vil bare fokusere på testdelen av dette biblioteket her. Vi så tidligere at å produsere stress på kode med flere tråder øker sjansene for å finne feil relatert til samtidighet.

Mens vi kan skrive verktøy for å produsere stresset selv, gir tempus-fugit praktiske måter å oppnå det samme på.

La oss se på den samme koden som vi prøvde å produsere stress for tidligere, og forstå hvordan vi kan oppnå det samme ved å bruke tempus-fugit:

offentlig klasse MyCounterTests {@Rule public ConcurrentRule concurrently = new ConcurrentRule (); @Rule public RepeatingRule rule = new RepeatingRule (); privat statisk MyCounter-teller = ny MyCounter (); @Test @Concurrent (count = 10) @Repeating (repetition = 10) public void runsMultipleTimes () {counter.increment (); } @AfterClass offentlig statisk ugyldig annotatedTestRunsMultipleTimes () kaster InterruptedException {assertEquals (counter.getCount (), 100); }}

Her bruker vi to av Regeler tilgjengelig for oss fra tempus-fugit. Disse reglene avskjærer testene og hjelper oss med å anvende ønsket atferd, som repetisjon og samtidighet. Så, effektivt, gjentar vi operasjonen under test ti ganger hver fra ti forskjellige tråder.

Når vi øker repetisjonen og samtidigheten, vil sjansene våre for å oppdage mangler relatert til samtidighet øke.

4.2. Trådvever

Trådvever er egentlig et Java-rammeverk for testing av flertrådet kode. Vi har tidligere sett at trådsammenfletting er ganske uforutsigbar, og derfor kan vi aldri finne visse feil gjennom regelmessige tester. Det vi faktisk trenger er en måte å kontrollere sammenflettene og teste all mulig sammenfletting. Dette har vist seg å være en ganske kompleks oppgave i vårt forrige forsøk.

La oss se hvordan trådvever kan hjelpe oss her. Thread Weaver lar oss flette ut kjøringen av to separate tråder på et stort antall måter, uten å måtte bekymre oss for hvordan. Det gir oss også muligheten for å ha finkornet kontroll over hvordan vi vil at trådene skal flettes inn.

La oss se hvordan vi kan forbedre vårt forrige, naive forsøk:

offentlig klasse MyCounterTests {private MyCounter-teller; @ThreadedBefore ugyldig før () {counter = new MyCounter (); } @ThreadedMain offentlig ugyldig mainThread () {counter.increment (); } @ThreadedSecondary public void secondThread () {counter.increment (); } @ThreadedAfter offentlig ugyldig etter () {assertEquals (2, counter.getCount ()); } @Test public void testCounter () {new AnnotatedTestRunner (). RunTests (this.getClass (), MyCounter.class); }}

Her har vi definert to tråder som prøver å øke telleren vår. Thread Weaver vil prøve å kjøre denne testen med disse trådene i alle mulige sammenflettingsscenarier. Muligens i en av interleaves, vil vi få feilen, noe som er ganske tydelig i vår kode.

4.3. MultithreadedTC

MultithreadedTC er enda et rammeverk for testing av samtidige applikasjoner. Den har en metronom som brukes til å gi fin kontroll over sekvensen av aktiviteter i flere tråder. Den støtter testsaker som utøver en spesifikk sammenfletting av tråder. Derfor bør vi ideelt sett være i stand til å teste hver betydelig sammenfletting i en egen tråd deterministisk.

Nå er en komplett introduksjon til dette funksjonsrike biblioteket utenfor omfanget av denne opplæringen. Men vi kan absolutt se hvordan vi raskt kan sette opp tester som gir oss mulige sammenflettinger mellom utførende tråder.

La oss se hvordan vi kan teste koden vår mer deterministisk med MultithreadedTC:

offentlig klasse MyTests utvider MultithreadedTestCase {privat MyCounter-teller; @Override public void initialize () {counter = new MyCounter (); } offentlig ugyldig tråd1 () kaster InterruptedException {counter.increment (); } offentlig ugyldig tråd2 () kaster InterruptedException {counter.increment (); } @ Override public void finish () {assertEquals (2, counter.getCount ()); } @Test offentlig ugyldig testCounter () kaster Throwable {TestFramework.runManyTimes (nye MyTests (), 1000); }}

Her setter vi opp to tråder for å operere på den delte telleren og øke den. Vi har konfigurert MultithreadedTC til å utføre denne testen med disse trådene i opptil tusen forskjellige sammenflettinger til den oppdager en som mislykkes.

4.4. Java jcstress

OpenJDK vedlikeholder Code Tool Project for å tilby utviklerverktøy for å jobbe med OpenJDK-prosjektene. Det er flere nyttige verktøy under dette prosjektet, inkludert Java Concurrency Stress Tests (jcstress). Dette blir utviklet som en eksperimentell sele og en rekke tester for å undersøke riktigheten av samtidighetsstøtte i Java.

Selv om dette er et eksperimentelt verktøy, kan vi fortsatt bruke dette til å analysere samtidig kode og skrive tester for å finansiere mangler relatert til den. La oss se hvordan vi kan teste koden vi har brukt så langt i denne opplæringen. Konseptet er ganske likt fra et bruksperspektiv:

@JCStressTest @Outcome (id = "1", forvent = ACCEPTABLE_INTERESTING, desc = "Én oppdatering tapt.") @Outcome (id = "2", expect = ACCEPTABLE, desc = "Begge oppdateringer.") @Stat offentlig klasse MyCounterTests {privat MyCounter-teller; @Actor offentlig ugyldig skuespiller1 () {counter.increment (); } @Actor public void actor2 () {counter.increment (); } @Arbiter offentlig ugyldig arbiter (I_Result r) {r.r1 = counter.getCount (); }}

Her har vi merket klassen med en kommentar Stat, som indikerer at den inneholder data som er mutert av flere tråder. Vi bruker også en kommentar Skuespiller, som markerer metodene som holder handlingene utført av forskjellige tråder.

Til slutt har vi en metode merket med en kommentar Arbiter, som egentlig bare besøker staten en gang Skuespillers har besøkt den. Vi har også brukt kommentar Utfall for å definere våre forventninger.

Samlet sett er oppsettet ganske enkelt og intuitivt å følge. Vi kan kjøre dette ved hjelp av en testsele, gitt av rammeverket, som finner alle klassene merket med JCStressTest og utfører dem i flere iterasjoner for å oppnå alle mulige sammenflettinger.

5. Andre måter å oppdage problemer med samtidighet

Å skrive tester for samtidig kode er vanskelig, men mulig. Vi har sett utfordringene og noen av de populære måtene å overvinne dem på. Derimot, vi kan ikke være i stand til å identifisere alle mulige problemer med samtidighet gjennom tester alene - spesielt når de økende kostnadene ved å skrive flere tester begynner å oppveie fordelene.

Derfor, sammen med et rimelig antall automatiserte tester, kan vi bruke andre teknikker for å identifisere problemer med samtidig. Dette vil øke sjansene våre for å finne problemer med samtidig uten å komme for mye dypere inn i kompleksiteten til automatiserte tester. Vi vil dekke noen av disse i denne delen.

5.1. Statisk analyse

Statisk analyse refererer til analysen av et program uten å faktisk utføre det. Nå, hva kan en slik analyse gjøre? Vi kommer til det, men la oss først forstå hvordan det står i kontrast til dynamisk analyse. Enhetstestene vi har skrevet så langt, må kjøres med faktisk gjennomføring av programmet de tester. Dette er grunnen til at de er en del av det vi i stor grad omtaler som dynamisk analyse.

Vær oppmerksom på at statisk analyse på ingen måte erstatning for dynamisk analyse. Det gir imidlertid et uvurderlig verktøy for å undersøke kodestrukturen og identifisere mulige feil lenge før vi til og med utfører koden. De statisk analyse bruker en rekke maler som er kuratert med erfaring og forståelse.

Selv om det er ganske mulig å bare se gjennom koden og sammenligne med beste praksis og regler vi har kuratert, må vi innrømme at det ikke er sannsynlig for større programmer. Det er imidlertid flere verktøy tilgjengelig for å utføre denne analysen for oss. De er ganske modne, med en stor kiste med regler for de fleste av de populære programmeringsspråkene.

Et utbredt verktøy for statisk analyse for Java er FindBugs. FindBugs ser etter forekomster av “bug mønstre”. Et feilmønster er et kodeidiom som ofte er en feil. Dette kan oppstå på grunn av flere grunner som vanskelige språkfunksjoner, misforståtte metoder og misforståtte invarianter.

FindBugs inspiserer Java bytecode for forekomst av feilmønstre uten å faktisk utføre bytekoden. Dette er ganske praktisk å bruke og raskt å kjøre. FindBugs rapporterer feil som tilhører mange kategorier som forhold, design og duplisert kode.

Det inkluderer også mangler relatert til samtidighet. Det må imidlertid bemerkes at FindBugs kan rapportere falske positive. Disse er færre i praksis, men må korreleres med manuell analyse.

5.2. Modellkontroll

Modellkontroll er en metode for å sjekke om en endelig tilstandsmodell av et system oppfyller en gitt spesifikasjon. Nå kan denne definisjonen høres for akademisk ut, men hold den med en stund!

Vi kan vanligvis representere et beregningsproblem som en endelig tilstandsmaskin. Selv om dette er et enormt område i seg selv, gir det oss en modell med et endelig sett med stater og overgangsregler mellom dem med klart definerte start- og slutttilstander.

Nå, den spesifikasjon definerer hvordan en modell skal oppføre seg for at den skal betraktes som riktig. I hovedsak inneholder denne spesifikasjonen alle kravene til systemet som modellen representerer. En av måtene å fange spesifikasjoner på er å bruke den tidsmessige logikkformelen, utviklet av Amir Pnueli.

Selv om det er logisk mulig å utføre modellkontroll manuelt, er det ganske upraktisk. Heldigvis er det mange verktøy tilgjengelig for å hjelpe oss her.Et slikt verktøy tilgjengelig for Java er Java PathFinder (JPF). JPF ble utviklet med mange års erfaring og forskning ved NASA.

Nærmere bestemt, JPF er en modellkontroll for Java bytecode. Det kjører et program på alle mulige måter, og derved sjekker for eiendomsbrudd som fastlåste og ubehandlede unntak langs alle mulige kjørestier. Det kan derfor vise seg å være ganske nyttig for å finne feil relatert til samtidighet i ethvert program.

6. Ettertanke

Nå skal det ikke være en overraskelse for oss at Det er best å unngå kompleksitet relatert til kode med flere tråder så mye som mulig. Å utvikle programmer med enklere design, som er lettere å teste og vedlikeholde, bør være vårt hovedmål. Vi må være enige om at samtidig programmering ofte er nødvendig for moderne applikasjoner.

Derimot, vi kan vedta flere beste praksis og prinsipper mens vi utvikler samtidige programmer som kan gjøre livet vårt lettere. I denne delen vil vi gå gjennom noen av disse beste metodene, men vi bør huske på at denne listen langt fra er komplett!

6.1. Reduser kompleksitet

Kompleksitet er en faktor som kan gjøre testing av et program vanskelig selv uten noen samtidige elementer. Dette bare forbindelser i møte med samtidighet. Det er ikke vanskelig å forstå hvorfor enklere og mindre programmer er lettere å resonnere om og dermed å teste effektivt. Det er flere beste mønstre som kan hjelpe oss her, som SRP (Single Responsibility Pattern) og KISS (Keep It Stupid Simple), for å bare nevne noen.

Nå, mens disse ikke tar opp spørsmålet om å skrive tester for samtidig kode direkte, gjør de jobben lettere å prøve.

6.2. Vurder Atomic Operations

Atomiske operasjoner er operasjoner som kjører helt uavhengig av hverandre. Derfor kan vanskelighetene med å forutsi og teste sammenfletting rett og slett unngås. Sammenlign og bytt er en slik mye brukt atominstruksjon. Enkelt sagt, det sammenligner innholdet på en minneplassering med en gitt verdi, og bare hvis det er det samme, endrer innholdet på det minneplasseringen.

De fleste moderne mikroprosessorer tilbyr en variant av denne instruksjonen. Java tilbyr en rekke atomklasser som AtomicInteger og AtomicBoolean, som gir fordelene med sammenlignings-og-bytte-instruksjoner under.

6.3. Omfavne uforanderlighet

I flertrådet programmering gir delte data som kan endres alltid rom for feil. Uforanderlighet refererer til tilstanden der en datastruktur ikke kan endres etter instantiering. Dette er en kamp laget i himmelen for samtidige programmer. Hvis tilstanden til et objekt ikke kan endres etter at den ble opprettet, trenger ikke konkurrerende tråder å søke om gjensidig utelukkelse av dem. Dette forenkler i stor grad å skrive og teste samtidige programmer.

Vær imidlertid oppmerksom på at vi kanskje ikke alltid har frihet til å velge uforanderlighet, men vi må velge det når det er mulig.

6.4. Unngå Delt minne

De fleste av problemene knyttet til flertrådet programmering kan tilskrives det faktum at vi har delt minne mellom konkurrerende tråder. Hva om vi bare kunne kvitte oss med dem! Vel, vi trenger fortsatt en mekanisme for at tråder skal kommunisere.

Det er alternative designmønstre for samtidige applikasjoner som gir oss denne muligheten. En av de populære er Actor Model, som foreskriver skuespilleren som den grunnleggende enheten for samtidighet. I denne modellen samhandler skuespillere med hverandre ved å sende meldinger.

Akka er et rammeverk skrevet i Scala som utnytter skuespillermodellen for å tilby bedre samtidige primitiver.

7. Konklusjon

I denne opplæringen dekket vi noen av de grunnleggende relaterte til samtidig programmering. Vi diskuterte multi-threaded concurrency i Java spesielt detaljert. Vi gikk gjennom utfordringene det gir oss mens vi testet en slik kode, spesielt med delte data. Videre gikk vi gjennom noen av verktøyene og teknikkene som er tilgjengelige for å teste samtidig kode.

Vi diskuterte også andre måter å unngå samtidighetsproblemer, inkludert verktøy og teknikker i tillegg til automatiserte tester. Til slutt gikk vi gjennom noen av de beste metodene for programmering relatert til samtidig programmering.

Kildekoden for denne artikkelen finner du på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found