Testiranje višenitnog koda na Javi

1. Uvod

U ovom ćemo uputstvu pokriti neke od osnova testiranja istodobnog programa. Primarno ćemo se usredotočiti na istodobnost temeljenu na nitima i probleme koje ona predstavlja prilikom testiranja.

Također ćemo razumjeti kako možemo riješiti neke od ovih problema i učinkovito testirati kod s više niti u Javi.

2. Istodobno programiranje

Istovremeno programiranje odnosi se na programiranje gdje mi razbiti velik dio proračuna na manje, relativno neovisne proračune.

Namjera je ove vježbe istodobno izvoditi ta manja izračunavanja, možda čak i paralelno. Iako postoji nekoliko načina da se to postigne, cilj je uvijek brže pokrenuti program.

2.1. Teme i istodobno programiranje

Budući da procesori spakiraju više jezgri nego ikad, istovremeno je programiranje u prvom planu kako bi ih učinkovito iskoristili. Međutim, činjenica ostaje ta istodobne programe puno je teže dizajnirati, pisati, testirati i održavati. Dakle, ako nakon svega možemo napisati učinkovite i automatizirane testove za istodobne programe, možemo riješiti velik dio tih problema.

Pa, što čini pisanje testova za istodobni kôd tako teškim? Da bismo to razumjeli, moramo razumjeti kako postižemo istodobnost u našim programima. Jedna od najpopularnijih tehnika istodobnog programiranja uključuje upotrebu niti.

Sada niti mogu biti izvorne, u tom ih slučaju planiraju osnovni operativni sustavi. Također možemo koristiti ono što je poznato kao zelene niti, koje se izravno izvršavaju u rasporedu.

2.2. Poteškoće u testiranju istodobnih programa

Bez obzira na to koju vrstu niti koristimo, ono što ih čini teškom za upotrebu je komunikacija niti. Ako doista uspijemo napisati program koji uključuje niti, ali ne i komunikaciju niti, nema ništa bolje! Realnije, niti će obično morati komunicirati. Postoje dva načina da se to postigne - zajednička memorija i prosljeđivanje poruka.

Glavnina problem povezan s istodobnim programiranjem proizlazi iz korištenja izvornih niti s dijeljenom memorijom. Testiranje takvih programa teško je iz istih razloga. Više niti s pristupom zajedničkoj memoriji obično zahtijeva međusobno izuzeće. To obično postižemo nekim zaštitnim mehanizmom pomoću brava.

No, ovo još uvijek može dovesti do mnoštva problema poput uvjeta utrke, brava pod naponom, mrtvih ulica i izgladnjivanja niti, da nabrojimo samo neke. Štoviše, ti su problemi povremeni, jer je raspoređivanje niti u slučaju izvornih niti potpuno nedeterminističko.

Stoga je pisanje učinkovitih testova za istodobne programe koji mogu otkriti ove probleme na deterministički način zaista izazov!

2.3. Anatomija prepletanja navoja

Znamo da izvorne niti operativni sustavi mogu nepredvidivo zakazati. U slučaju da ovi niti pristupaju i mijenjaju zajedničke podatke, što dovodi do zanimljivog prepletanja niti. Iako su neka od ovih preplitanja možda potpuno prihvatljiva, drugi konačne podatke mogu ostaviti u nepoželjnom stanju.

Uzmimo primjer. Pretpostavimo da imamo globalni brojač koji se uvećava za svaku nit. Na kraju obrade željeli bismo da stanje ovog brojača bude potpuno jednako broju izvršenih niti:

privatni int brojač; prirast javne praznine () {counter ++; }

Sada, do prirast primitivnog cijelog broja u Javi nije atomska operacija. Sastoji se od čitanja vrijednosti, povećanja i konačno spremanja. Iako više niti radi istu operaciju, to može dovesti do mnogih mogućih prepletanja:

Iako ovo posebno preplitanje daje potpuno prihvatljive rezultate, što kažete na ovo:

To nije ono što smo očekivali. Sada zamislite stotine niti koje pokreću kod koji je puno složeniji od ovog. To će stvoriti nezamislive načine na koje će se niti ispreplesti.

Postoji nekoliko načina za pisanje koda koji izbjegava ovaj problem, ali to nije tema ovog vodiča. Sinkronizacija pomoću brave jedna je od čestih, ali ima svojih problema povezanih s uvjetima utrke.

3. Ispitivanje koda s više niti

Sad kad razumijemo osnovne izazove u testiranju višenitnog koda, vidjet ćemo kako ih prevladati. Izgradit ćemo jednostavan slučaj upotrebe i pokušati simulirati što više problema povezanih s istodobnošću.

Počnimo s definiranjem jednostavne klase koja broji možda išta:

javna klasa MyCounter {private int count; prirast javne praznine () {int temp = count; count = temp + 1; } // Dobivač za brojanje}

Ovo je naizgled bezazlen komad koda, ali nije teško shvatiti da nije sigurno. Ako slučajno napišemo istodobni program s ovom klasom, sigurno će biti neispravan. Svrha ispitivanja ovdje je utvrditi takve nedostatke.

3.1. Ispitivanje nes istodobnih dijelova

U pravilu, uvijek je preporučljivo testirati kod izolirajući ga od bilo kakvog istodobnog ponašanja. Na ovaj se način razumno može utvrditi da u kodu nema drugih nedostataka koji nisu povezani sa istodobnošću. Pogledajmo kako to možemo učiniti:

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

Iako se ovdje ne događa baš ništa, ovaj test daje nam sigurnost da djeluje barem u nedostatku istodobnosti.

3.2. Prvi pokušaj istodobnog testiranja

Krenimo s ponovnim testiranjem istog koda, ovaj put u istodobnom postavljanju. Pokušat ćemo pristupiti istoj instanci ove klase s više niti i vidjeti kako se ponaša:

@Test public void testCounterWithConcurrency () baca InterruptedException {int numberOfThreads = 10; Usluga ExecutorService = Executors.newFixedThreadPool (10); Zasun CountDownLatch = novi CountDownLatch (brojOfThreads); MyCounter brojač = novi MyCounter (); for (int i = 0; i {counter.increment (); latch.countDown ();}); } zasun.čekajte (); assertEquals (numberOfThreads, counter.getCount ()); }

Ovaj je test razuman jer pokušavamo upravljati dijeljenim podacima s nekoliko niti. Kako držimo broj niti na niskom, poput 10, primijetit ćemo da on prolazi gotovo cijelo vrijeme. Zanimljivo, ako započnemo povećavati broj niti, recimo na 100, vidjet ćemo da test većinu vremena počinje propadati.

3.3. Bolji pokušaj istodobnog testiranja

Iako je prethodni test otkrio da naš kôd nije siguran u nit, postoji problem s tim cuclom. Ovaj test nije deterministički jer se temeljne niti prepliću na nedeterministički način. Doista se ne možemo osloniti na ovaj test za naš program.

Ono što trebamo je način za kontrolu preplitanja niti tako da možemo otkriti probleme s istodobnošću na deterministički način s mnogo manje niti. Započet ćemo podešavanjem koda koji testiramo:

javni sinkronizirani void inkrement () baca InterruptedException {int temp = count; pričekati (100); count = temp + 1; }

Evo, napravili smo metodu sinkronizirano i uveo čekanje između dva koraka unutar metode. The sinkronizirano Ključna riječ osigurava da samo jedna nit može mijenjati računati varijabla odjednom, a čekanje uvodi kašnjenje između svakog izvođenja niti.

Napominjemo da ne moramo nužno mijenjati kôd koji namjeravamo testirati. Međutim, budući da ne postoji mnogo načina na koje možemo utjecati na raspoređivanje niti, pribjegavamo tome.

U kasnijem odjeljku vidjet ćemo kako to možemo učiniti bez promjene koda.

Ajmo sada, testirajmo slično ovom kodu kao i ranije:

@Test public void testSummationWithConcurrency () baca InterruptedException {int numberOfThreads = 2; Usluga ExecutorService = Executors.newFixedThreadPool (10); Zasun CountDownLatch = novi CountDownLatch (brojOfThreads); MyCounter brojač = novi MyCounter (); for (int i = 0; i {try {counter.increment ();} catch (InterruptedException e) {// Obrada iznimke} latch.countDown ();}); } zasun.čekajte (); assertEquals (numberOfThreads, counter.getCount ()); }

Evo, ovo radimo samo s dvije niti, a šanse su da ćemo uspjeti dobiti nedostatak koji nam je nedostajao. Ovdje smo pokušali postići specifično prepletanje niti, za koje znamo da može utjecati na nas. Iako dobar za demonstraciju, ovo nam možda neće biti korisno u praktične svrhe.

4. Dostupni alati za testiranje

Kako raste broj niti, mogući broj načina na koje se mogu prepletati raste eksponencijalno. To je jednostavno nije moguće odgonetnuti sva takva prepletanja i testirati ih. Moramo se osloniti na alate koji će poduzeti isti ili sličan napor za nas. Srećom, na raspolaganju ih je nekoliko koji će nam olakšati život.

Dvije su nam široke kategorije alata dostupne za testiranje istodobnog koda. Prva nam omogućuje da stvorimo razumno velik stres na istodobni kôd s mnogo niti. Stres povećava vjerojatnost rijetkog preplitanja i, tako, povećava naše šanse za pronalaženje nedostataka.

Drugi nam omogućuje da simuliramo specifično preplitanje niti, pomažući nam tako da s većom sigurnošću pronađemo nedostatke.

4.1. tempus-fugit

Java knjižnica tempus-fugit pomaže nam s lakoćom pisati i testirati istodobni kôd. Ovdje ćemo se samo usredotočiti na testni dio ove knjižnice. Ranije smo vidjeli da stvaranje stresa na kodu s više niti povećava šanse za pronalaženje nedostataka povezanih s istodobnošću.

Iako možemo pisati uslužne programe da sami stvorimo stres, tempus-fugit pruža prikladne načine za postizanje istog.

Ponovno posjetimo isti kod za koji smo ranije pokušali stvoriti stres i shvatimo kako to možemo postići pomoću tempus-fugit:

javna klasa MyCounterTests {@Rule public ConcurrentRule concurrently = new ConcurrentRule (); @Rule javno pravilo ponavljanjaRule = novo RepeatingRule (); privatni statički brojač MyCounter = novi MyCounter (); @Test @Concurrent (count = 10) @Ponavljanje (ponavljanje = 10) public void runsMultipleTimes () {counter.increment (); } @AfterClass javna statička praznina annotatedTestRunsMultipleTimes () baca InterruptedException {assertEquals (counter.getCount (), 100); }}

Ovdje koristimo dva od Pravilodostupni su nam iz tempus-fugit. Ova pravila presreću testove i pomažu nam primijeniti željena ponašanja, poput ponavljanja i istodobnosti. Dakle, učinkovito ponavljamo ispitanu operaciju deset puta po deset iz deset različitih niti.

Kako povećavamo ponavljanje i istodobnost, povećavat će se naše šanse za otkrivanje nedostataka povezanih s istodobnošću.

4.2. Tkač niti

Thread Weaver je u osnovi Java okvir za testiranje višenitnog koda. Ranije smo vidjeli da je preplitanje niti prilično nepredvidljivo, pa stoga možda redovitim testovima nikada nećemo pronaći određene nedostatke. Ono što nam zapravo treba je način za kontrolu preplitanja i testiranje svih mogućih prepletanja. To se pokazalo prilično složenim zadatkom u našem prethodnom pokušaju.

Pogledajmo kako nam Thread Weaver može ovdje pomoći. Thread Weaver omogućuje nam međusobno preplitanje izvršavanja dviju zasebnih niti na velik broj načina, bez brige o tome kako. Također nam daje mogućnost precizne kontrole nad načinom na koji želimo da se niti isprepleću.

Pogledajmo kako možemo poboljšati svoj prethodni, naivni pokušaj:

javna klasa MyCounterTests {privatni brojač MyCounter; @ThreadedBefore javna void before () {counter = new MyCounter (); } @ThreadedMain javna praznina mainThread () {counter.increment (); } @ThreadedSecondary public void secondThread () {counter.increment (); } @ThreadedAfter javna void nakon () {assertEquals (2, counter.getCount ()); } @Test public void testCounter () {new AnnotatedTestRunner (). RunTests (this.getClass (), MyCounter.class); }}

Ovdje smo definirali dvije niti koje pokušavaju povećati naš brojač. Thread Weaver pokušat će pokrenuti ovaj test s ovim nitima u svim mogućim scenarijima preplitanja. Moguće je da ćemo u jednom od preplitanja dobiti kvar, što je sasvim očito u našem kodu.

4.3. VišenitniTC

VišenitniTC je još jedan okvir za testiranje istodobnih aplikacija. Sadrži metronom koji se koristi za pružanje fine kontrole nad redoslijedom aktivnosti u više niti. Podržava test slučajeve koji vrše određeno preplitanje niti. Stoga bismo idealno trebali biti u mogućnosti testirati svako značajno prepletanje u zasebnoj niti deterministički.

Sad, cjelovit uvod u ovu biblioteku bogatu značajkama izvan je dosega ovog vodiča. Ali, zasigurno možemo vidjeti kako brzo postaviti testove koji nam pružaju moguća prepletanja između izvršavanja niti.

Pogledajmo kako možemo više testirati svoj kod s MultithreadedTC-om:

javna klasa MyTests proširuje MultithreadedTestCase {privatni brojač MyCounter; @Override public void initialize () {counter = new MyCounter (); } public void thread1 () baca InterruptedException {counter.increment (); } public void thread2 () baca InterruptedException {counter.increment (); } @Override public void finish () {assertEquals (2, counter.getCount ()); } @Test public void testCounter () baca mogućnost bacanja {TestFramework.runManyTimes (novi MyTests (), 1000); }}

Ovdje postavljamo dvije niti za rad na dijeljenom brojaču i njegovo povećanje. Konfigurirali smo MultithreadedTC da izvrši ovaj test s ovim nitima za do tisuću različitih preplitanja dok ne otkrije jedan koji ne uspije.

4.4. Java jcstress

OpenJDK održava Code Tool Project kako bi pružio alate za programere za rad na projektima OpenJDK. U okviru ovog projekta postoji nekoliko korisnih alata, uključujući Java testove istodobnosti stresa (jcstress). Ovo se razvija kao eksperimentalni uprtač i paket testova za ispitivanje ispravnosti podrške istodobnosti u Javi.

Iako je ovo eksperimentalni alat, još uvijek ga možemo iskoristiti za analizu istodobnog koda i pisanje testova za financiranje kvarova povezanih s njim. Pogledajmo kako možemo testirati kod koji smo do sada koristili u ovom vodiču. Koncept je prilično sličan iz perspektive upotrebe:

@JCStressTest @Outcome (id = "1", očekivati ​​= ACCEPTABLE_INTERESTING, desc = "Jedno ažuriranje izgubljeno.") @Outcome (id = "2", očekivati ​​= PRIHVATLJIVO, desc = "Oba ažuriranja.") @ Državna javna klasa MyCounterTests {privatni brojač MyCounter; @Glumac javni void glumac1 () {counter.increment (); } @Actor javni void glumac2 () {counter.increment (); } @Arbiter javni vojni arbitar (I_Result r) {r.r1 = counter.getCount (); }}

Evo, označili smo razred napomenom država, što znači da sadrži podatke koji su mutirani od više niti. Također, koristimo bilješku Glumac, koji označava metode koje zadržavaju radnje izvršene različitim nitima.

Napokon, imamo metodu označenu bilješkom Arbitar, koja u biti samo jednom posjećuje državu Glumacs posjetili su ga. Također smo koristili bilješke Ishod definirati naša očekivanja.

Sve u svemu, postava je vrlo jednostavna i intuitivna za praćenje. To možemo pokrenuti pomoću ispitnog pojasa, zadanog u okviru, koji pronalazi sve klase označene s JCStressTest i izvršava ih u nekoliko ponavljanja kako bi se dobila sva moguća isprepletanja.

5. Drugi načini za otkrivanje istodobnih problema

Pisanje testova za istodobni kod je teško, ali moguće. Vidjeli smo izazove i neke od popularnih načina kako ih prevladati. Međutim, možda samo testovima nećemo moći prepoznati sve moguće probleme s istodobnošću - pogotovo kad dodatni troškovi pisanja dodatnih testova počnu nadmašivati ​​njihove koristi.

Stoga, zajedno s razumnim brojem automatiziranih testova, možemo upotrijebiti druge tehnike za prepoznavanje problema s istodobnošću. To će povećati naše šanse za pronalaženje paralelnih problema, a da ne ulazimo previše u složenost automatiziranih testova. Neke ćemo od njih pokriti u ovom odjeljku.

5.1. Statička analiza

Statička analiza odnosi se na analizu programa bez stvarnog izvođenja. Sad, kakve koristi može imati takva analiza? Doći ćemo do toga, ali hajde prvo da shvatimo kako je to u suprotnosti s dinamičkom analizom. Jedinstvene testove koje smo do sada napisali potrebno je pokrenuti sa stvarnim izvršavanjem programa koji testiraju. To je razlog zašto su oni dio onoga što mi uglavnom nazivamo dinamičkom analizom.

Imajte na umu da statička analiza ni na koji način nije zamjena za dinamičku analizu. Međutim, pruža neprocjenjiv alat za ispitivanje strukture koda i prepoznavanje mogućih nedostataka mnogo prije nego što uopće izvršimo kôd. The statička analiza koristi mnoštvo predložaka koji su kurirani iskustvom i razumijevanje.

Iako je sasvim moguće samo pregledati kôd i usporediti najbolje prakse i pravila koja smo pripremili, moramo priznati da to nije vjerojatno za veće programe. Međutim, postoji nekoliko alata dostupnih za obavljanje ove analize za nas. Prilično su zreli, s velikim brojem pravila za većinu popularnih programskih jezika.

Prevladavajući alat za statičku analizu za Javu je FindBugs. FindBugs traži primjere "obrazaca bugova". Uzorak grešaka idiom je koda koji je često pogreška. To se može pojaviti iz nekoliko razloga poput teških jezičnih značajki, pogrešno shvaćenih metoda i pogrešno shvaćenih invarijanata.

FindBugs pregledava Java bajtkod za pojave uzoraka grešaka a da zapravo ne izvrši bajt kod. Ovo je vrlo prikladno za upotrebu i brzo se izvodi. FindBugs prijavljuje bugove koji pripadaju mnogim kategorijama poput uvjeta, dizajna i dupliciranog koda.

Također uključuje nedostatke povezane s istodobnošću. Međutim, mora se napomenuti da FindBugs može prijaviti lažno pozitivne rezultate. U praksi ih je manje, ali moraju biti u korelaciji s ručnom analizom.

5.2. Provjera modela

Provjera modela je metoda provjere zadovoljava li model konačnog stanja sustava zadanu specifikaciju. Ova definicija možda zvuči previše akademski, ali podnesite je neko vrijeme!

Računski problem obično možemo predstavljati kao stroj s konačnim stanjima. Iako je ovo samo po sebi veliko područje, daje nam model s konačnim skupom stanja i pravilima prijelaza između njih s jasno definiranim početnim i završnim stanjima.

Sada, specifikacija definira kako se model treba ponašati da bi se smatrao ispravnim. U osnovi, ova specifikacija sadrži sve zahtjeve sustava koji model predstavlja. Jedan od načina za hvatanje specifikacija je upotreba vremenske logičke formule, koju je razvio Amir Pnueli.

Iako je logično moguće ručno izvršiti provjeru modela, prilično je nepraktično. Srećom, ovdje nam je na raspolaganju mnogo alata.Jedan takav alat dostupan za Javu je Java PathFinder (JPF). JPF je razvijen s dugogodišnjim iskustvom i istraživanjima u NASA-i.

Posebno, JPF je alat za provjeru Java bajt koda. Pokreće program na sve moguće načine, provjeravajući time kršenja imovine poput zastoja i neobrađenih iznimaka na svim mogućim putovima izvršenja. Stoga se može pokazati vrlo korisnim u pronalaženju nedostataka povezanih s istodobnošću u bilo kojem programu.

6. Naknadne misli

Do sada nas to ne bi trebalo čuditi najbolje je izbjegavati složenost vezanu uz kod s više niti koliko je god moguće. Naš glavni cilj trebao bi biti razvoj programa jednostavnijeg dizajna, koje je lakše testirati i održavati. Moramo se složiti da je istodobno programiranje često potrebno za moderne aplikacije.

Međutim, možemo usvojiti nekoliko najboljih praksi i načela dok razvijamo istodobne programe koji nam mogu olakšati život. U ovom ćemo odjeljku proći kroz neke od ovih najboljih praksi, ali trebali bismo imati na umu da ovaj popis još uvijek nije potpun!

6.1. Smanjite složenost

Složenost je faktor koji može otežati testiranje programa čak i bez istodobnih elemenata. To se samo spoji suočavajući se s istodobnošću. Nije teško razumjeti zašto jednostavnije i manje programe lakše je rasuđivati, a time i učinkovito testirati. Postoji nekoliko najboljih uzoraka koji nam ovdje mogu pomoći, poput SRP-a (Single Responsibility Pattern) i KISS (Keep It Stupid Simple), da nabrojimo samo neke.

Sada, iako se ovi ne bave izravno pitanjem pisanja testova za istodobni kôd, oni olakšavaju posao.

6.2. Razmislite o atomskim operacijama

Atomske operacije su operacije koje se odvijaju potpuno neovisno jedna o drugoj. Stoga se poteškoće predviđanja i testiranja prepletanja mogu jednostavno izbjeći. Usporedite i zamijenite jedna je od tako često korištenih atomskih uputa. Jednostavno rečeno, uspoređuje sadržaj memorijskog mjesta s danom vrijednošću i, samo ako su isti, mijenja sadržaj tog memorijskog mjesta.

Većina modernih mikroprocesora nudi neku varijantu ove upute. Java nudi niz atomskih klasa poput AtomicInteger i AtomicBoolean, nudeći pogodnosti usporedbe i zamjene uputa u nastavku.

6.3. Prigrlite nepromjenjivost

U programiranju s više niti, zajednički podaci koji se mogu mijenjati uvijek ostavljaju mjesta pogreškama. Nepromjenljivost odnosi se na stanje kada se struktura podataka ne može mijenjati nakon instanciranja. Ovo je meč napravljen na nebu za istodobne programe. Ako se stanje objekta ne može promijeniti nakon stvaranja, konkurentske niti se ne moraju prijaviti za međusobno izuzeće na njima. To uvelike pojednostavljuje pisanje i testiranje istodobnih programa.

Međutim, imajte na umu da možda nećemo uvijek imati slobodu odabrati nepromjenjivost, ali moramo se odlučiti za nju kad je to moguće.

6.4. Izbjegavajte dijeljenu memoriju

Većina problema vezanih uz programiranje s više niti može se pripisati činjenici da dijelimo memoriju između konkurentskih niti. Što kad bismo ih se jednostavno mogli riješiti! Pa, još uvijek trebamo neki mehanizam za komunikaciju niti.

Tamo su zamjenski uzorci dizajna za istodobne aplikacije koji nam nude ovu mogućnost. Jedan od popularnih je Model glumca koji glumca propisuje kao osnovnu jedinicu podudarnosti. U ovom modelu glumci međusobno komuniciraju slanjem poruka.

Akka je okvir napisan u Scali koji koristi model glumca kako bi ponudio bolje primitivnosti paralelnosti.

7. Zaključak

U ovom smo tutorijalu pokrili neke od osnova povezanih s istodobnim programiranjem. Posebno smo razgovarali o višenitnoj istodobnosti u Javi. Prošli smo kroz izazove koje nam predstavlja tijekom testiranja takvog koda, posebno s dijeljenim podacima. Nadalje, prošli smo neke alate i tehnike dostupne za testiranje istodobnog koda.

Također smo razgovarali o drugim načinima kako izbjeći probleme s istodobnošću, uključujući alate i tehnike osim automatiziranih testova. Na kraju smo prošli kroz neke najbolje prakse programiranja povezane s istodobnim programiranjem.

Izvorni kod za ovaj članak može se naći na GitHubu.


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