Ohjelmointi

Java-ketjujen ohjelmointi tosielämässä, osa 1

Kaikki muut Java-ohjelmat kuin yksinkertaiset konsolipohjaiset sovellukset ovat monisäikeisiä, haluatko tai et. Ongelmana on, että Abstract Windowing Toolkit (AWT) käsittelee käyttöjärjestelmän (OS) tapahtumia omalla säikeellään, joten kuuntelijasi menetelmät toimivat tosiasiallisesti AWT-ketjussa. Nämä samat kuuntelumenetelmät käyttävät tyypillisesti objekteja, joihin pääsee myös pääkierteestä. Tässä vaiheessa voi olla houkuttelevaa haudata pääsi hiekkaan ja teeskennellä, että sinun ei tarvitse huolehtia langan aiheista, mutta et yleensä pääse siitä eroon. Ja valitettavasti käytännössä mikään Java-kirjoista ei käsittele ketjuttamisongelmia riittävän perusteellisesti. (Katso luettelo hyödyllisiä kirjoja aiheesta, katso Resurssit.)

Tämä artikkeli on ensimmäinen sarjassa, joka esittelee todellisia ratkaisuja Java-ohjelmoinnin ongelmiin monisäikeisessä ympäristössä. Se on suunnattu Java-ohjelmoijille, jotka ymmärtävät kielitason jutut ( synkronoitu avainsana ja Lanka luokka), mutta haluat oppia käyttämään näitä kieliominaisuuksia tehokkaasti.

Alustariippuvuus

Valitettavasti Javan lupaus alustan riippumattomuudesta putoaa kasvoilleen säikeiden areenalla. Vaikka on mahdollista kirjoittaa alustasta riippumaton monisäikeinen Java-ohjelma, sinun on tehtävä se silmäsi auki. Tämä ei oikeastaan ​​ole Javan vika; on lähes mahdotonta kirjoittaa todella alustasta riippumatonta ketjutusjärjestelmää. (Doug Schmidtin ACE-kehys [Adaptive Communication Environment] on hyvä, vaikkakin monimutkainen yritys. Katso linkki hänen ohjelmaan lähteistä.) Joten, ennen kuin voin puhua Java-ohjelmointikysymyksistä myöhemmissä erissä, minun on keskustella alustojen aiheuttamista vaikeuksista, joilla Java-virtuaalikone (JVM) saattaa toimia.

Atomienergia

Ensimmäinen käyttöjärjestelmän tason käsite, joka on tärkeä ymmärtää, on atomisuus. Atomioperaatiota ei voida keskeyttää toisella säikeellä. Java määrittelee ainakin muutaman atomioperaation. Erityisesti kohdistaminen minkä tahansa tyyppisiin muuttujiin paitsi pitkä tai kaksinkertainen on atominen. Sinun ei tarvitse huolehtia siitä, että ketju ennakoi menetelmää tehtävän keskellä. Käytännössä tämä tarkoittaa, että sinun ei koskaan tarvitse synkronoida menetelmää, joka ei tee muuta kuin palauttaa arvon (tai antaa arvon) looginen tai int instanssimuuttuja. Samoin menetelmää, joka teki paljon laskelmia käyttäen vain paikallisia muuttujia ja argumentteja ja joka osoitti kyseisen laskennan tulokset instanssimuuttujalle viimeisenä tekemästään, ei tarvitse synkronoida. Esimerkiksi:

luokka some_class {int jotkut_kenttä; void f (some_class arg) // tarkoituksella ei synkronoitu {// Tee täällä paljon sellaista, joka käyttää paikallisia muuttujia // ja metodiargumentteja, mutta ei pääse // mihinkään luokan kenttiin (tai kutsu mitään menetelmiä //, jotka käyttävät mitä tahansa luokan kentät). // ... jotkut_kenttä = uusi_arvo; // tee tämä viimeinen. }} 

Toisaalta, kun suoritetaan x = ++ y tai x + = y, sinut voidaan ennaltaehkäistä lisäyksen jälkeen, mutta ennen tehtävää. Käytä atomiasettia tässä tilanteessa käyttämällä avainsanaa synkronoitu.

Kaikki tämä on tärkeää, koska synkronoinnin yleiskustannukset voivat olla ei-triviaaleja ja ne voivat vaihdella käyttöjärjestelmästä toiseen. Seuraava ohjelma osoittaa ongelman. Jokainen silmukka kutsuu toistuvasti menetelmää, joka suorittaa samat toiminnot, mutta yhtä menetelmistä (lukitus()) synkronoidaan ja toinen (ei lukitusta ()) ei ole. Käyttämällä Windows NT 4: ssä toimivaa JDK "performance-pack" -MM-ohjelmaa ohjelma ilmoittaa kahden silmukan välillä olevan 1,2 sekunnin ajonaikainen ero tai noin 1,2 mikrosekuntia puhelua kohden. Tämä ero ei ehkä näytä kovin suurelta, mutta se edustaa 7,25 prosentin lisäystä puheluaikaa. Tietenkin prosentuaalinen lisäys putoaa, kun menetelmä tekee enemmän työtä, mutta huomattava määrä menetelmiä - ainakin ohjelmissani - ovat vain muutama koodirivi.

tuo java.util. *; luokan synkronointi {  synkronoitu int-lukitus (int a, int b) {return a + b;} int not_locking (int a, int b) {return a + b;}  yksityinen staattinen lopullinen int ITERAATIOT = 1000000; staattinen julkinen void main (String [] args) {synch tester = new synch (); tuplakäynnistys = uusi päivämäärä (). getTime ();  (pitkä i = ITERAATIOT; --i> = 0;) testaajalle. lukitus (0,0);  double end = uusi päivämäärä (). getTime (); kaksoislukitusaika = loppu - alku; alku = uusi päivämäärä (). getTime ();  varten (pitkä i = ITERAATIOT; --i> = 0;) testaaja. ei lukitusta (0,0);  end = uusi päivämäärä (). getTime (); double not_locking_time = loppu - alku; double time_in_synchronization = lukitusaika - ei lukituksen_aika; System.out.println ("Synkronoinnille menetetty aika (millis.):" + Time_in_synchronization); System.out.println ("Lukituksen yleiskustannukset per puhelu:" + (time_in_synkronointi / ITERATION)); System.out.println (ei_lukitusaika / lukitusaika * 100,0 + "%: n lisäys"); }} 

Vaikka HotSpot-virtuaalikoneen on tarkoitus ratkaista synkronointi-yleisongelma, HotSpot ei ole ilmaispalvelu - sinun on ostettava se. Ellet lisensoi ja lähetä HotSpotia sovelluksesi kanssa, ei ole mitään tietoa siitä, mikä virtuaalikone on kohdealustalla, ja tietysti haluat, että mahdollisimman vähän ohjelman suorittamisnopeudesta riippuu sitä suorittavasta virtuaalikoneesta. Vaikka umpikujaan liittyviä ongelmia (joista keskustelen tämän sarjan seuraavassa erässä) ei ollut olemassa, käsitys, että sinun pitäisi "synkronoida kaikki", on yksinkertaisesti vääräpää.

Samanaikaisuus vs. rinnakkaisuus

Seuraava käyttöjärjestelmään liittyvä asia (ja pääongelma alustasta riippumattoman Java: n kirjoittamisen yhteydessä) liittyy käsitteisiin samanaikaisuus ja rinnakkaisuus. Samanaikaiset monisäikeiset järjestelmät antavat vaikutelman useista tehtävistä, jotka suoritetaan kerralla, mutta nämä tehtävät todella jaetaan paloiksi, jotka jakavat prosessorin muiden tehtävien palojen kanssa. Seuraava kuva kuvaa asioita. Rinnakkaisissa järjestelmissä kaksi tehtävää suoritetaan samanaikaisesti. Rinnakkaisuus vaatii usean suorittimen järjestelmän.

Ellet viettää paljon aikaa estettynä odottaessasi I / O-toimintojen päättymistä, useita samanaikaisia ​​ketjuja käyttävä ohjelma toimii usein hitaammin kuin vastaava yksisäikeinen ohjelma, vaikka se on usein paremmin järjestetty kuin vastaava yksittäinen ketju. -säikeiversio. Ohjelma, joka käyttää useita ketjuja, jotka kulkevat samanaikaisesti useissa prosessoreissa, toimii paljon nopeammin.

Vaikka Java sallii ketjutuksen toteuttamisen kokonaan virtuaalikoneessa, ainakin teoriassa, tämä lähestymistapa sulkisi pois sovelluksen rinnakkaisuuden. Jos mitään käyttöjärjestelmän tason ketjuja ei käytetä, käyttöjärjestelmä katsoisi VM-esiintymää yksisäikeisenä sovelluksena, joka todennäköisesti ajoitetaan yhdelle prosessorille. Nettotulos olisi, että kaksi samaa VM-esiintymää käyttävää Java-ketjua ei koskaan toimisi rinnakkain, vaikka sinulla olisi useita suorittimia ja virtuaalikoneesi olisi ainoa aktiivinen prosessi. Kaksi erillistä sovellusta käyttävän virtuaalikoneen tapausta voisi tietysti toimia rinnakkain, mutta haluan tehdä sen paremmin. Saadaksesi rinnakkaisuuden, VM on pakko kartoittaa Java-ketjut käyttöjärjestelmän ketjuihin; joten sinulla ei ole varaa jättää huomiotta eroja eri ketjumallien välillä, jos alustan riippumattomuus on tärkeää.

Selvitä prioriteettisi suoraan

Esittelen tapoja, joilla juuri keskustelemani kysymykset voivat vaikuttaa ohjelmiisi vertaamalla kahta käyttöjärjestelmää: Solaris ja Windows NT.

Ainakin teoriassa Java tarjoaa kymmenen prioriteettitasoa ketjuille. (Jos kaksi tai useampia ketjuja odottaa suoritusta, se, jolla on korkein prioriteettitaso, suoritetaan.) Solarisissa, joka tukee 231 prioriteettitasoa, tämä ei ole ongelma (vaikka Solariksen prioriteetteja voi olla hankala käyttää - lisää tästä hetkessä). NT: llä on sitä vastoin seitsemän prioriteettitasoa, jotka on kartoitettava Java: n kymmeneen. Tätä kartoitusta ei ole määritelty, joten on paljon mahdollisuuksia. (Esimerkiksi Java-prioriteettitasot 1 ja 2 saattavat sekä kartoittaa NT-prioriteettitasolle 1 että Java-prioriteettitasot 8, 9 ja 10 saattavat kaikki kartoittaa NT-tasolle 7.)

NT: n prioriteettitasojen vähyys on ongelma, jos haluat käyttää prioriteettia aikataulutuksen hallintaan. Asiat tekee vielä monimutkaisemmaksi se, että prioriteettitasoja ei ole vahvistettu. NT tarjoaa mekanismin nimeltä prioriteetin lisääminen, jonka voit kytkeä pois päältä C-järjestelmäkutsulla, mutta et Java-ohjelmasta. Kun prioriteetin lisääminen on käytössä, NT lisää langan prioriteettia määrittelemättömällä määrällä määrittelemättömän ajan joka kerta, kun se suorittaa tiettyjä I / O-järjestelmäkutsuja. Käytännössä tämä tarkoittaa, että ketjun prioriteettitaso voi olla korkeampi kuin luulet, koska tämä ketju sattui suorittamaan I / O-operaation hankalana aikana.

Ensisijaisen tehostamisen tarkoituksena on estää taustankäsittelyä tekeviä ketjuja vaikuttamasta käyttöliittymän raskaiden tehtävien ilmeiseen reagoivuuteen. Muissa käyttöjärjestelmissä on kehittyneempiä algoritmeja, jotka tyypillisesti alentavat taustaprosessien prioriteettia. Tämän järjestelmän haittapuoli, varsinkin kun se toteutetaan lankakohtaisesti eikä prosessikohtaisesti, on se, että prioriteetin avulla on erittäin vaikea määrittää, milloin tietty ketju suoritetaan.

Se pahenee.

Solarisissa, kuten kaikissa Unix-järjestelmissä, prosesseilla on etusija sekä ketjut. Korkean prioriteetin prosessien säikeitä ei voida keskeyttää matalan prioriteetin prosessien ketjuilla. Lisäksi järjestelmänvalvoja voi rajoittaa tietyn prosessin prioriteettitasoa, jotta käyttäjäprosessi ei keskeytä kriittisiä käyttöjärjestelmän prosesseja. NT ei tue mitään tästä. NT-prosessi on vain osoiteavaruus. Sillä ei ole prioriteettia sinänsä, eikä sitä ole ajoitettu. Järjestelmä ajoittaa ketjut; sitten, jos tietty ketju on käynnissä prosessissa, jota ei ole muistissa, prosessi vaihdetaan sisään. NT-langan prioriteetit jakautuvat useisiin "prioriteettiluokkiin", jotka jaetaan todellisten prioriteettien jatkuvuuteen. Järjestelmä näyttää tältä:

Sarakkeet ovat todellisia prioriteettitasoja, joista kaikkien on jaettava vain 22. (Muita NT käyttää itse.) Rivit ovat prioriteettiluokkia. Keskeneräiset prioriteettiluokassa sidotut prosessissa olevat ketjut ovat käynnissä tasoilla 1-6 ja 15 riippuen niille määritetystä loogisesta prioriteettitasosta. Normaaliksi prioriteettiluokaksi sidotun prosessin säikeet toimivat tasoilla 1, 6 - 10 tai 15, jos prosessilla ei ole tulokohdistusta. Jos sillä on tulokeskeisyys, ketjut kulkevat tasoilla 1, 7 - 11 tai 15. Tämä tarkoittaa, että tyhjäkäynnillä olevan prioriteettiluokan prosessin korkean prioriteetin säike voi ennakoida normaalin prioriteettiluokan prosessin matalan prioriteetin säikeen, mutta vain jos prosessi on käynnissä taustalla. Huomaa, että "korkean" prioriteettiluokan prosessissa on käytettävissä vain kuusi prioriteettitasoa. Muilla luokilla on seitsemän.

NT ei tarjoa tapaa rajoittaa prosessin prioriteettiluokkaa. Mikä tahansa koneen minkä tahansa prosessin ketju voi ottaa haltuunsa laatikon hallinnan milloin tahansa lisäämällä omaa prioriteettiluokkaansa; tätä ei voida puolustaa.

Tekninen termi, jota käytän NT: n prioriteetin kuvaamiseen, on epäpyhä sotku. Käytännössä prioriteetti on käytännössä arvoton NT: n alla.

Joten mitä ohjelmoija tekee? NT: n rajoitetun prioriteettitasojen määrän ja hallitsemattoman prioriteettikorotuksen välillä ei ole mitään turvallista tapaa Java-ohjelmalle käyttää prioriteettitasoja aikataulutuksessa. Yksi toimiva kompromissi on rajoittaa itsesi Lanka.MAX_PRIORITY, Lanka.MIN_PRIORITYja Lanka.NORM_PRIORITY kun soitat setPriority (). Tämä rajoitus ainakin välttää 10-tasoisen - 7-tasoisen ongelman. Oletan, että voisit käyttää os.nimi järjestelmäominaisuus NT: n havaitsemiseksi ja kutsu sitten natiivimenetelmä prioriteettikorostuksen poistamiseksi käytöstä, mutta se ei toimi, jos sovelluksesi toimii Internet Explorerissa, ellet käytä myös Sunin VM-laajennusta. (Microsoftin virtuaalikoneessa käytetään epätyypillistä natiivimenetelmän toteutusta.) Joka tapauksessa inhoan natiivimenetelmien käyttöä. Välttän yleensä ongelman mahdollisimman paljon asettamalla useimmat säikeet NORM_PRIORITY ja muiden ajoitusmekanismien kuin prioriteetin käyttäminen. (Keskustelen näistä näistä tämän sarjan tulevissa erissä.)

Tehdä yhteistyötä!

Käyttöjärjestelmissä on tyypillisesti kaksi ketjumallia: yhteistyöhön perustuva ja ennakoiva.

Osuuskunnan monisäikeinen malli

Jonkin sisällä osuuskunta järjestelmässä ketju säilyttää prosessorin hallinnan, kunnes se päättää luopua siitä (mikä ei ehkä koskaan ole koskaan). Eri säikeiden on tehtävä yhteistyötä toistensa kanssa tai kaikki, paitsi yksi säikeistä "nälkää" (eli ei koskaan annettu mahdollisuutta juosta). Aikataulu tehdään useimmissa osuuskuntajärjestelmissä tiukasti prioriteettitason mukaan. Kun nykyinen säie luopuu hallinnasta, korkeimman prioriteetin odotuslanka saa hallinnan. (Poikkeus tähän sääntöön on Windows 3.x, joka käyttää osuuskuntamallia, mutta jolla ei ole paljon ajastinta. Kohdistettu ikkuna saa hallinnan.)

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