Programmering

Programmering av Java-tråder i den virkelige verden, del 1

Alle andre Java-programmer enn enkle konsollbaserte applikasjoner er flertrådet, enten du liker det eller ikke. Problemet er at Abstract Windowing Toolkit (AWT) behandler operativsystemhendelser (OS) på sin egen tråd, slik at lyttermetodene dine faktisk kjører på AWT-tråden. Disse samme lyttermetodene får vanligvis tilgang til objekter som også er tilgjengelige fra hovedtråden. Det kan på dette punktet være fristende å begrave hodet i sanden og late som om du ikke trenger å bekymre deg for å trekke problemer, men du kan vanligvis ikke komme unna med det. Og dessverre adresserer praktisk talt ingen av bøkene på Java trådspørsmål i tilstrekkelig dybde. (For en liste over nyttige bøker om emnet, se Ressurser.)

Denne artikkelen er den første i en serie som vil presentere virkelige løsninger på problemene med programmering av Java i et flertrådet miljø. Den er rettet mot Java-programmerere som forstår språknivå ting ( synkronisert søkeord og de forskjellige fasilitetene i Tråd klasse), men ønsker å lære hvordan du bruker disse språkfunksjonene effektivt.

Plattformavhengighet

Dessverre faller Java sitt løfte om plattformuavhengighet på tråden. Selv om det er mulig å skrive et plattformuavhengig flertrådet Java-program, må du gjøre det med åpne øyne. Dette er egentlig ikke Java sin feil; det er nesten umulig å skrive et virkelig plattformuavhengig trådsystem. (Doug Schmidts ACE [Adaptive Communication Environment] rammeverk er et godt, men komplekst, forsøk. Se ressurser for en lenke til programmet hans.) Før jeg kan snakke om hard-core Java-programmeringsproblemer i påfølgende avdrag, må jeg diskutere vanskeligheter som plattformene Java-maskinen (JVM) kan kjøre på.

Atomenergi

Det første konseptet på OS-nivå som er viktig å forstå er atomisitet. En atomoperasjon kan ikke avbrytes av en annen tråd. Java definerer i det minste noen få atomoperasjoner. Spesielt tildeling til variabler av hvilken som helst type unntatt lang eller dobbelt er atomisk. Du trenger ikke å bekymre deg for en tråd som forutsetter en metode midt i oppgaven. I praksis betyr dette at du aldri trenger å synkronisere en metode som ikke gjør annet enn å returnere verdien av (eller tilordne en verdi til) a boolsk eller int forekomstvariabel. Tilsvarende trenger en metode som gjorde mye beregning med bare lokale variabler og argumenter, og som tildelte resultatene av denne beregningen til en forekomstvariabel som det siste den gjorde, ikke å bli synkronisert. For eksempel:

klasse noen_klasse {int noen_felt; ugyldig f (some_class arg) // bevisst ikke synkronisert {// Gjør mange ting her som bruker lokale variabler // og metodeargumenter, men ikke får tilgang til // noen felt i klassen (eller kaller noen metoder // som får tilgang til noen felt i klassen). // ... noe_felt = nytt_verdi; // gjør dette sist. }} 

På den annen side, når du kjører x = ++ y eller x + = y, kan du bli forhåndsbestemt etter økningen, men før oppdraget. For å få atomicitet i denne situasjonen, må du bruke nøkkelordet synkronisert.

Alt dette er viktig fordi synkroniseringskostnadene kan være ikke-private, og kan variere fra OS til OS. Følgende program viser problemet. Hver sløyfe kaller gjentatte ganger en metode som utfører de samme operasjonene, men en av metodene (låse()) er synkronisert og den andre (not_locking ()) er ikke. Ved hjelp av JDK "performance-pack" VM som kjører under Windows NT 4, rapporterer programmet en forskjell på 1,2 sekunder i kjøretid mellom de to sløyfene, eller omtrent 1,2 mikrosekunder per samtale. Denne forskjellen virker kanskje ikke så mye, men den representerer en økning på 7,25 prosent i ringetiden. Selvfølgelig faller den prosentvise økningen etter hvert som metoden fungerer mer, men et betydelig antall metoder - i hvert fall i programmene mine - er bare noen få kodelinjer.

importer java.util. *; klassesynkronisering {  synkronisert int-låsing (int a, int b) {return a + b;} int not_locking (int a, int b) {return a + b;}  privat statisk slutt int ITERASJONER = 1000000; statisk public void main (String [] args) {synch tester = new synch (); dobbelt start = ny dato (). getTime ();  for (lang i = ITERASJONER; --i> = 0;) tester.locking (0,0);  dobbel slutt = ny dato (). getTime (); dobbel locking_time = slutt - start; start = ny dato (). getTime ();  for (lang i = ITERASJONER; --i> = 0;) tester. ikke_låsing (0,0);  slutt = ny dato (). getTime (); dobbelt not_locking_time = slutt - start; dobbel tid_synkronisering = låsetid - ikke_låstid; System.out.println ("Tapt tid til synkronisering (millis.):" + Time_in_synchronization); System.out.println ("Låser overhead per samtale:" + (tid_synkronisering / ITERASJONER)); System.out.println (not_locking_time / locking_time * 100.0 + "% økning"); }} 

Selv om HotSpot VM skal løse problemet med synkronisering, er HotSpot ikke et freebee - du må kjøpe det. Med mindre du lisensierer og sender HotSpot med appen din, vet du ikke hva VM vil være på målplattformen, og selvfølgelig vil du ha så lite som mulig av kjøringshastigheten til programmet ditt for å være avhengig av den virtuelle maskinen som utfører den. Selv om det ikke eksisterte problemer med fastlåste forhold (som jeg vil diskutere i neste del av denne serien), er forestillingen om at du skal "synkronisere alt" rett og slett feilhodet.

Samtidighet kontra parallellitet

Det neste OS-relaterte problemet (og hovedproblemet når det gjelder å skrive plattformuavhengig Java) har å gjøre med forestillingene om samtidighet og parallellitet. Samtidige flertrådingssystemer ser ut som flere oppgaver som utføres samtidig, men disse oppgavene er faktisk delt opp i biter som deler prosessoren med biter fra andre oppgaver. Følgende figur illustrerer problemstillingene. I parallelle systemer utføres to oppgaver faktisk samtidig. Parallelisme krever et fler-CPU-system.

Med mindre du bruker mye tid sperret og venter på at I / O-operasjoner skal fullføres, vil et program som bruker flere samtidige tråder ofte kjøre saktere enn et tilsvarende program med en tråd, selv om det ofte vil være bedre organisert enn tilsvarende enkelt -trådversjon. Et program som bruker flere tråder som kjører parallelt på flere prosessorer, vil kjøre mye raskere.

Selv om Java tillater at threading kan implementeres helt i VM, i det minste i teorien, vil denne tilnærmingen utelukke enhver parallellitet i applikasjonen din. Hvis ingen tråder på operativsystemnivå ble brukt, vil operativsystemet se på VM-forekomsten som et enkelt-trådt program, som mest sannsynlig vil bli planlagt til en enkelt prosessor. Nettoresultatet ville være at ingen Java-tråder som kjører under samme VM-forekomst, noen gang vil kjøre parallelt, selv om du hadde flere CPUer og den virtuelle maskinen din var den eneste aktive prosessen. To forekomster av at VM kjører separate applikasjoner, kan kjøres parallelt, selvfølgelig, men jeg vil gjøre det bedre enn det. For å få parallellitet, VM kartlegg Java-tråder til OS-tråder; så du har ikke råd til å ignorere forskjellene mellom de forskjellige trådmodellene hvis plattformuavhengighet er viktig.

Få prioriteringene dine rett

Jeg vil demonstrere hvordan problemene jeg nettopp har diskutert, kan påvirke programmene dine ved å sammenligne to operativsystemer: Solaris og Windows NT.

Java gir i det minste ti prioritetsnivåer for tråder. (Hvis to eller flere tråder begge venter på å kjøre, vil den med høyest prioritetsnivå utføres.) I Solaris, som støtter 231 prioritetsnivåer, er dette ikke noe problem (selv om Solaris-prioriteringer kan være vanskelig å bruke - mer om dette om et øyeblikk). NT har derimot syv prioritetsnivåer tilgjengelig, og disse må kartlegges til Java sine ti. Denne kartleggingen er udefinert, så mange muligheter presenterer seg. (For eksempel kan Java-prioritetsnivå 1 og 2 begge tilordnes til NT-prioritetsnivå 1, og Java-prioritetsnivå 8, 9 og 10 kan alle tilordnes til NT-nivå 7.)

NTs mangel på prioritetsnivåer er et problem hvis du vil bruke prioritet til å kontrollere planlegging. Ting blir enda mer komplisert av at prioritetsnivåene ikke er faste. NT gir en mekanisme som kalles prioritering øke, som du kan slå av med et C-systemanrop, men ikke fra Java. Når prioritetsøkning er aktivert, øker NT en tråds prioritet med et ubestemt beløp i en ubestemt tid hver gang den utfører visse I / O-relaterte systemanrop. I praksis betyr dette at en tråds prioritetsnivå kan være høyere enn du tror fordi den tråden tilfeldigvis utførte en I / O-operasjon på en vanskelig tid.

Poenget med den prioriterte økningen er å forhindre at tråder som gjør bakgrunnsbehandling, påvirker den tilsynelatende responsen til UI-tunge oppgaver. Andre operativsystemer har mer sofistikerte algoritmer som vanligvis senker prioriteten til bakgrunnsprosesser. Ulempen med denne ordningen, spesielt når den implementeres på en per-tråd i stedet for per-prosess-nivå, er at det er veldig vanskelig å bruke prioritet for å bestemme når en bestemt tråd skal kjøres.

Det blir verre.

I Solaris, som er tilfelle i alle Unix-systemer, har prosesser prioritet så vel som tråder. Trådene til prosesser med høy prioritet kan ikke avbrytes av trådene til prosesser med lav prioritet. Videre kan prioritetsnivået til en gitt prosess begrenses av en systemadministrator slik at en brukerprosess ikke vil avbryte kritiske OS-prosesser. NT støtter ikke noe av dette. En NT-prosess er bare et adresserom. Den har ingen prioritet i seg selv, og er ikke planlagt. Systemet planlegger tråder; så, hvis en gitt tråd kjører under en prosess som ikke er i minnet, byttes prosessen inn. NT-trådprioriteter faller inn i forskjellige "prioritetsklasser", som fordeles over et kontinuum av faktiske prioriteringer. Systemet ser slik ut:

Kolonnene er faktiske prioritetsnivåer, bare 22 må deles av alle applikasjoner. (De andre brukes av NT selv.) Radene er prioritetsklasser. Trådene som kjøres i en prosess som er festet i ledig prioritetsklasse, kjører på nivå 1 til 6 og 15, avhengig av det tildelte logiske prioritetsnivået. Trådene til en prosess festet som normal prioritetsklasse vil kjøre på nivå 1, 6 til 10 eller 15 hvis prosessen ikke har inngangsfokus. Hvis det har inngangsfokus, kjøres trådene på nivå 1, 7 til 11 eller 15. Dette betyr at en høyprioritert tråd i en inaktiv prioritetsklasseprosess kan forhindre en lavprioritetstråd i en normal prioritetsklasseprosess, men bare hvis prosessen kjører i bakgrunnen. Legg merke til at en prosess som kjører i "høy" prioritetsklasse bare har seks prioritetsnivåer tilgjengelig. De andre klassene har syv.

NT gir ingen måte å begrense prioritetsklassen til en prosess. Enhver tråd på hvilken som helst prosess på maskinen kan når som helst overta kontrollen over boksen ved å øke sin egen prioritetsklasse; det er ikke noe forsvar mot dette.

Det tekniske begrepet jeg bruker for å beskrive NTs prioritet er uhellig rot. I praksis er prioritering praktisk talt verdiløs under NT.

Så hva skal en programmerer gjøre? Mellom NTs begrensede antall prioritetsnivåer og det er ukontrollerbar prioritering, er det ingen absolutt trygg måte for et Java-program å bruke prioritetsnivåer for planlegging. Et gjennomførbart kompromiss er å begrense deg til Tråd.MAX_PRIORITY, Tråd.MIN_PRIORITY, og Tråd.NORM_PRIORITY når du ringer setPriority (). Denne begrensningen unngår i det minste problemet med 10-nivåer-kartlagt-til-7-nivåer. Jeg antar at du kan bruke os.name systemegenskap for å oppdage NT, og deretter ringe til en innfødt metode for å slå av prioritetsøkning, men det fungerer ikke hvis appen din kjører under Internet Explorer, med mindre du også bruker Suns VM-plugin. (Microsofts VM bruker en ikke-standard implementering av native-metoden.) I hvert fall hater jeg å bruke native-metoder. Jeg unngår vanligvis problemet så mye som mulig ved å sette de fleste tråder på NORM_PRIORITY og bruke andre planleggingsmekanismer enn prioritet. (Jeg vil diskutere noen av disse i fremtidige avdrag av denne serien.)

Samarbeide!

Det er vanligvis to trådmodeller som støttes av operativsystemer: samarbeidende og preemptive.

Den kooperative flertrådingsmodellen

I en kooperativ systemet beholder en tråd kontrollen over prosessoren til den bestemmer seg for å gi den opp (som kanskje aldri blir). De forskjellige trådene må samarbeide med hverandre, eller alle trådene unntatt en vil bli "sultet" (som betyr, aldri gitt en sjanse til å løpe). Planlegging av de fleste samarbeidssystemer gjøres strengt etter prioritetsnivå. Når den gjeldende tråden gir opp kontrollen, får den ventende tråden med høyest prioritet kontroll. (Et unntak fra denne regelen er Windows 3.x, som bruker en samarbeidsmodell, men som ikke har mye av en planlegger. Vinduet som har fokus får kontroll.)

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