Programovanie

Programovanie vlákien Java v reálnom svete, 1. časť

Všetky programy Java iné ako jednoduché konzolové aplikácie majú viacvláknové vlákno, či sa vám to páči alebo nie. Problém je v tom, že program Abstract Windowing Toolkit (AWT) spracúva udalosti operačného systému (OS) na svojom vlastnom vlákne, takže vaše metódy poslucháča skutočne bežia na vlákne AWT. Tieto rovnaké metódy poslucháča zvyčajne pristupujú k objektom, ku ktorým sa tiež pristupuje z hlavného vlákna. V tomto okamihu môže byť lákavé zaboriť hlavu do piesku a predstierať, že sa nemusíte trápiť problémami so závitmi, ale obvykle sa z toho nedokážete dostať. A, bohužiaľ, prakticky žiadna z kníh o Jave nerieši problémy s vláknami v dostatočnej hĺbke. (Zoznam užitočných kníh o tejto téme nájdete v časti Zdroje.)

Tento článok je prvým zo série, ktorá predstaví riešenia problémov programovania Javy v prostredí s viacerými vláknami v reálnom svete. Je určený pre programátorov v jazyku Java, ktorí rozumejú jazykovým znalostiam ( synchronizované kľúčové slovo a rôzne možnosti Závit triedy), ale chcete sa naučiť, ako tieto jazykové funkcie efektívne využívať.

Závislosť na platforme

Sľub Java, že nezávislosť na platforme, bohužiaľ, padá na zem v aréne vlákien. Aj keď je možné napísať na platforme nezávislý viacvláknový program Java, musíte to robiť s otvorenými očami. Toto v skutočnosti nie je chyba Java; Je takmer nemožné napísať skutočne platformovo nezávislý systém vlákien. (Rámec ACE [Adaptive Communication Environment] od Doug Schmidta je dobrý, aj keď zložitý pokus. Odkaz na jeho program nájdete v Zdrojoch.) Takže predtým, ako budem môcť hovoriť o náročných problémoch s programovaním v Jave v ďalších inštaláciách, musím diskutovať o problémoch, ktoré prinášajú platformy, na ktorých môže bežať virtuálny stroj Java (JVM).

Atómová energia

Prvý koncept na úrovni OS, ktorý je dôležité pochopiť, je atomicita. Atómovú operáciu nemožno prerušiť iným vláknom. Java definuje aspoň niekoľko atómových operácií. Najmä priradenie k premenným akéhokoľvek typu okrem dlho alebo dvojitý je atómový. Nemusíte sa obávať, že vlákno upustí od metódy uprostred úlohy. V praxi to znamená, že nikdy nemusíte synchronizovať metódu, ktorá nerobí nič iné, ako vrátiť hodnotu (alebo priradiť hodnotu) boolean alebo int inštančná premenná. Podobne by sa nemusela synchronizovať metóda, ktorá vykonávala veľa výpočtov iba s použitím lokálnych premenných a argumentov a ktorá ako posledná vec, ktorá priradila výsledky tohto výpočtu k inštančnej premennej, nemusela byť synchronizovaná. Napríklad:

trieda some_class {int some_field; void f (some_class arg) // úmyselne nesynchronizované {// Robte tu veľa vecí, ktoré používajú lokálne premenné // a argumenty metód, ale nepristupujú k // žiadnym poliam triedy (alebo nezavolajú žiadne metódy //, ktoré pristupujú k akýmkoľvek polia triedy). // ... some_field = new_value; // urob to naposledy. }} 

Na druhej strane pri exekúcii x = ++ y alebo x + = r, mohlo by sa vám predísť po zvýšení, ale pred priradením. Ak chcete v tejto situácii dosiahnuť atomicitu, budete musieť použiť kľúčové slovo synchronizované.

To všetko je dôležité, pretože réžia synchronizácie môže byť netriviálna a môže sa líšiť od operačného systému. Nasledujúci program demonštruje problém. Každá slučka opakovane volá metódu, ktorá vykonáva rovnaké operácie, ale jednu z metód (zamykanie ()) je synchronizovaný a druhý (not_locking ()) nie je. Pomocou virtuálneho počítača JDK „performance-pack“ bežiaceho pod Windows NT 4 program hlási 1,2-sekundový rozdiel v behu medzi týmito dvoma slučkami, alebo približne 1,2 mikrosekundy na hovor. Tento rozdiel sa nemusí zdať veľa, ale predstavuje 7,25-percentné zvýšenie doby hovoru. Percentuálne zvýšenie samozrejme klesá, keď metóda pracuje viac, ale značný počet metód - aspoň v mojich programoch - predstavuje iba niekoľko riadkov kódu.

import java.util. *; synchronizácia triedy {  synchronizované int uzamknutie (int a, int b) {return a + b;} int not_locking (int a, int b) {return a + b;}  súkromný statický konečný int ITERÁCIE = 10 000 000; static public void main (String [] args) {testovač synchronizácie = nová synchronizácia (); dvojitý štart = nový Date (). getTime ();  pre (dlhé i = ITERÁCIE; --i> = 0;) tester.locking (0,0);  dvojitý koniec = nový dátum (). getTime (); dvojité zamykanie_čas = koniec - štart; start = new Date (). getTime ();  pre (dlhé i = ITERÁCIE; --i> = 0;) tester.not_locking (0,0);  koniec = nový dátum (). getTime (); double not_locking_time = koniec - štart; double time_in_synchronization = uzamknutie_času - nie_blokovania_času; System.out.println ("Čas stratený synchronizáciou (v mil.):" + Time_in_synchronization); System.out.println ("Uzamykanie réžie na hovor:" + (time_in_synchronization / ITERATIONS)); System.out.println (not_locking_time / zamykanie_time * 100,0 + "% nárast"); }} 

Aj keď má HotSpot VM riešiť problém synchronizácie a réžie, HotSpot nie je freebee - musíte si ho kúpiť. Pokiaľ licenciu a dodanie služby HotSpot s vašou aplikáciou neurčíte, aký bude VM na cieľovej platforme, a samozrejme chcete, aby rýchlosť spustenia vášho programu bola čo najmenej závislá od VM, ktoré ho vykonáva. Aj keď problémy so zablokovaním (o ktorých sa budem zaoberať v ďalšom pokračovaní tejto série) neexistovali, predstava, že by ste mali „všetko synchronizovať“, je jednoducho mylná.

Súbežnosť verzus paralelizmus

Ďalší problém súvisiaci s OS (a hlavný problém, pokiaľ ide o písanie Java nezávislej na platforme), súvisí s predstavami súbežnosť a paralelizmus. Súbežné systémy s viacerými vláknami poskytujú vzhľad niekoľkých úloh, ktoré sa vykonávajú naraz, ale tieto úlohy sú v skutočnosti rozdelené na bloky, ktoré zdieľajú procesor s blokmi z iných úloh. Nasledujúci obrázok ilustruje problémy. V paralelných systémoch sa v skutočnosti vykonávajú dve úlohy súčasne. Paralelizmus vyžaduje systém s viacerými procesormi.

Ak nestrávite veľa času blokovaným a čakaním na dokončenie operácií I / O, program, ktorý používa viac súbežných vlákien, bude často bežať pomalšie ako ekvivalentný program s jedným vláknom, aj keď bude často lepšie organizovaný ako ekvivalentný jeden -vláknová verzia. Program, ktorý používa viac vlákien bežiacich paralelne na viacerých procesoroch, bude fungovať oveľa rýchlejšie.

Aj keď Java umožňuje implementáciu vlákien úplne na VM, aspoň teoreticky, tento prístup by vylúčil akýkoľvek paralelizmus vo vašej aplikácii. Ak by sa nepoužili žiadne vlákna na úrovni operačného systému, OS by sa na inštanciu VM díval ako na aplikáciu s jedným vláknom, ktorá by bola s najväčšou pravdepodobnosťou naplánovaná na jeden procesor. Čistým výsledkom by bolo, že nikdy nebudú paralelne bežať žiadne dve vlákna Java bežiace pod rovnakou inštanciou VM, aj keby ste mali viac CPU a váš VM bol jediný aktívny proces. Dve inštancie VM, na ktorých sú spustené samostatné aplikácie, samozrejme mohli bežať paralelne, ale chcem to urobiť lepšie. Ak chcete získať paralelizmus, VM musieť mapujte vlákna Java cez vlákna OS; takže si nemôžete dovoliť ignorovať rozdiely medzi rôznymi modelmi vlákien, ak je dôležitá nezávislosť na platforme.

Dostaňte svoje priority na pravú mieru

Predvediem, ako môžu problémy, o ktorých som práve hovoril, ovplyvniť vaše programy porovnaním dvoch operačných systémov: Solaris a Windows NT.

Java, prinajmenšom teoreticky, poskytuje vláknam desať úrovní priority. (Ak dva alebo viac vlákien čakajú na spustenie, spustí sa ten s najvyššou úrovňou priority.) V systéme Solaris, ktorý podporuje 231 úrovní priority, to nie je žiadny problém (hoci priority systému Solaris môžu byť zložité - viac o tom za chvíľu). Na druhej strane NT má k dispozícii sedem úrovní priority a tieto musia byť namapované do desiatich Java. Toto mapovanie nie je definované, takže sa ponúka veľa možností. (Napríklad úrovne priority Java 1 a 2 sa môžu mapovať na úroveň priority NT 1 a úrovne priority 8, 9 a 10 Java sa môžu všetky mapovať na úroveň NT 7.)

Nedostatok NT v úrovniach priority je problémom, ak chcete na kontrolu plánovania použiť prioritu. Veci sa ešte komplikujú tým, že úrovne priorít nie sú pevne dané. NT poskytuje mechanizmus tzv zvýšenie priority, ktoré môžete vypnúť pomocou systémového volania C, ale nie z Javy. Keď je povolené zvýšenie priority, NT zvýši prioritu vlákna o neurčitú dobu na neurčitý čas zakaždým, keď vykoná určité systémové volania súvisiace s I / O. V praxi to znamená, že úroveň priority vlákna môže byť vyššia, ako si myslíte, pretože toto vlákno náhodou vykonalo I / O operáciu v nepríjemnom čase.

Zmyslom zvýšenia priority je zabrániť tomu, aby vlákna, ktoré robia spracovanie na pozadí, mali vplyv na zjavnú odozvu úloh náročných na UI. Ostatné operačné systémy majú sofistikovanejšie algoritmy, ktoré zvyčajne znižujú prioritu procesov na pozadí. Nevýhodou tejto schémy, najmä ak je implementovaná na úrovni jednotlivých vlákien, a nie na úrovni jednotlivých procesov, je to, že je veľmi ťažké použiť prioritu na určenie, kedy bude konkrétne vlákno spustené.

Zhoršuje sa to.

V systéme Solaris, ako je to vo všetkých systémoch Unix, majú procesy prioritu aj vlákna. Vlákna procesov s vysokou prioritou nemôžu byť prerušená vláknami procesov s nízkou prioritou. Úroveň priority daného procesu môže byť navyše obmedzená správcom systému tak, aby používateľský proces neprerušil kritické procesy operačného systému. NT nič z toho nepodporuje. Proces NT je iba adresný priestor. Nemá žiadnu prioritu ako takú a nie je naplánovaná. Systém plánuje vlákna; potom, ak dané vlákno beží pod procesom, ktorý nie je v pamäti, je proces vymenený. Priority vlákna NT spadajú do rôznych „tried priorít“, ktoré sú distribuované v kontinuu skutočných priorít. Systém vyzerá takto:

Stĺpce sú skutočné úrovne priority, iba 22 z nich musí zdieľať všetky aplikácie. (Ostatné používa samotný NT.) Riadky sú prioritnými triedami. Vlákna bežiace v procese viazanom na triedu priority nečinnosti sú spustené na úrovniach 1 až 6 a 15, v závislosti od ich priradenej úrovne logickej priority. Vlákna procesu viazaného na normálnu prioritnú triedu budú bežať na úrovniach 1, 6 až 10 alebo 15, ak proces nemá vstupné zameranie. Ak má vstupné zameranie, vlákna prebiehajú na úrovniach 1, 7 až 11 alebo 15. To znamená, že vlákno s vysokou prioritou procesu triedy nečinnosti priority môže predísť vláknu s nízkou prioritou procesu triedy normálnej priority, ale iba ak je tento proces spustený na pozadí. Všimnite si, že proces bežiaci v triede „vysokej“ priority má k dispozícii iba šesť úrovní priority. Ostatné triedy majú sedem.

NT neposkytuje nijaký spôsob obmedzenia prioritnej triedy procesu. Akékoľvek vlákno v ktoromkoľvek procese na stroji môže kedykoľvek prevziať kontrolu nad boxom vylepšením svojej vlastnej prioritnej triedy; proti tomu neexistuje obrana.

Odborný termín, ktorým popisujem prioritu NT, je nesvätý neporiadok. V praxi je priorita v prípade NT prakticky bezcenná.

Čo má robiť programátor? Medzi obmedzeným počtom úrovní priority NT a jeho nekontrolovateľným zvyšovaním priorít neexistuje program Java, ktorý by používal úrovne priorít na plánovanie, absolútne bezpečný spôsob. Jedným z uskutočniteľných kompromisov je obmedziť sa na Vlákno.MAX_PRIORITY, Vlákno.MIN_PRIORITYa Vlákno.NORM_PRIORITY keď zavoláš setPriority (). Toto obmedzenie sa aspoň vyhýba problémom s 10 úrovňami mapovanými na 7 úrovní. Predpokladám, že by ste mohli použiť os.name vlastnosť systému na detekciu NT a potom zavolajte natívnu metódu na vypnutie zvyšovania priority, ale to nebude fungovať, ak je vaša aplikácia spustená v prehliadači Internet Explorer, pokiaľ nepoužívate aj doplnok VM spoločnosti Sun. (VM spoločnosti Microsoft používa neštandardnú implementáciu natívnej metódy.) V každom prípade nerád používam natívne metódy. Väčšinou sa problémom čo najviac vyhnem umiestnením väčšiny vlákien NORM_PRIORITY a použitie iných plánovacích mechanizmov ako priorít. (Niektoré z nich rozoberiem v budúcich častiach tejto série.)

Spolupracujte!

Operačné systémy podporujú zvyčajne dva modely vlákien: kooperatívny a preventívny.

Kooperatívny model viacerých vlákien

V družstevné systému si vlákno zachováva kontrolu nad svojim procesorom, kým sa ho nerozhodne vzdať (čo by mohlo byť nikdy). Rôzne vlákna musia navzájom spolupracovať alebo všetky vlákna okrem jednej budú „hladované“ (to znamená, že im nikdy nebola poskytnutá šanca na spustenie). Plánovanie vo väčšine kooperatívnych systémov sa vykonáva striktne podľa úrovne priority. Keď sa súčasné vlákno vzdá kontroly, získa kontrolu vlákno s najvyššou prioritou. (Výnimkou z tohto pravidla je Windows 3.x, ktorý používa model spolupráce, ale nemá príliš veľa plánovača. Ovládanie má okno, ktoré je zamerané.)

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