Programování

Programování vláken Java v reálném světě, 1. část

Všechny programy Java jiné než jednoduché konzolové aplikace mají více vláken, ať se vám to líbí nebo ne. Problém je v tom, že AWT (Abstract Windowing Toolkit) zpracovává události operačního systému na svém vlastním vlákně, takže vaše metody posluchače skutečně běží na vlákně AWT. Tyto stejné metody posluchače obvykle přistupují k objektům, ke kterým se přistupuje také z hlavního vlákna. V tomto okamžiku může být lákavé zabořit hlavu do písku a předstírat, že se nemusíš starat o problémy s vlákny, ale obvykle se z toho nedostaneš. A bohužel prakticky žádná z knih o Javě neřeší problémy se závitem v dostatečné hloubce. (Seznam užitečných knih k tomuto tématu najdete v části Zdroje.)

Tento článek je první ze série, která představí reálná řešení problémů programování prostředí Java ve vícevláknovém prostředí. Je určen pro programátory Java, kteří rozumějí jazykovým znalostem ( synchronizované klíčové slovo a různá zařízení Vlákno třídy), ale chcete se naučit, jak tyto jazykové funkce efektivně využívat.

Závislost na platformě

Slib Java nezávislosti na platformě bohužel v aréně vláken padl na tvář. Ačkoli je možné napsat na platformě nezávislý vícevláknový program Java, musíte to dělat s otevřenýma očima. Není to opravdu chyba Javy; je téměř nemožné napsat skutečně podprocesový systém nezávislý na platformě. (Rámec ACE [Adaptive Communication Environment] od Doug Schmidta je dobrý, i když složitý pokus. Odkaz na jeho program najdete v části Zdroje.) Takže než budu moci v dalších instalacích hovořit o zásadních problémech s programováním v Javě, diskutovat o obtížích způsobených platformami, na kterých by mohl běžet virtuální stroj Java (JVM).

Atomová energie

První koncept na úrovni OS, který je důležité pochopit, je atomicita. Atomovou operaci nelze přerušit jiným vláknem. Java definuje alespoň několik atomových operací. Zejména přiřazení k proměnným jakéhokoli typu kromě dlouho nebo dvojnásobek je atomová. Nemusíte si dělat starosti s tím, že vlákno preference metody uprostřed úkolu. V praxi to znamená, že nikdy nemusíte synchronizovat metodu, která nedělá nic jiného než vrátit hodnotu (nebo přiřadit hodnotu) booleovský nebo int proměnná instance. Podobně by nemusela být synchronizována metoda, která prováděla mnoho výpočtů pouze s použitím lokálních proměnných a argumentů a která přiřadila výsledky tohoto výpočtu proměnné instance jako poslední věc, kterou provedla. Například:

třída some_class {int some_field; void f (some_class arg) // záměrně nesynchronizováno {// Provádějte zde spoustu věcí, které používají lokální proměnné // a argumenty metod, ale nepřistupují // k žádným polím třídy (nebo nezavolají žádné metody //, které přistupují k jakýmkoli pole třídy). // ... some_field = new_value; // udělejte to naposledy. }} 

Na druhou stranu při provádění x = ++ y nebo x + = y, můžete být přednostní po přírůstku, ale před přiřazením. Chcete-li v této situaci získat atomicitu, budete muset použít klíčové slovo synchronizované.

To vše je důležité, protože režie synchronizace může být netriviální a může se u jednotlivých operačních systémů lišit. Následující program ukazuje problém. Každá smyčka opakovaně volá metodu, která provádí stejné operace, ale jedna z metod (zamykání ()) je synchronizován a druhý (not_locking ()) není. Pomocí virtuálního počítače JDK "performance-pack" běžícího pod Windows NT 4 program hlásí 1,2-sekundový rozdíl v běhu mezi dvěma smyčkami, nebo přibližně 1,2 mikrosekundy na volání. Tento rozdíl se nemusí zdát příliš velký, ale představuje 7,25% nárůst doby volání. Procentní nárůst samozřejmě klesá, protože metoda pracuje více, ale značný počet metod - alespoň v mých programech - je jen několik řádků kódu.

import java.util. *; synchronizace třídy {  synchronizované int zamykání (int a, int b) {return a + b;} int not_locking (int a, int b) {return a + b;}  soukromý statický konečný int ITERACE = 10 000 000; static public void main (String [] args) {synch tester = new synch (); double start = new Date (). getTime ();  for (long i = ITERATIONS; --i> = 0;) tester.locking (0,0);  dvojitý konec = nový Date (). getTime (); dvojitý zamykání_čas = konec - začátek; start = new Date (). getTime ();  for (long i = ITERATIONS; --i> = 0;) tester.not_locking (0,0);  end = new Date (). getTime (); double not_locking_time = end - start; double time_in_synchronization = zamykání_čas - ne_zamykání_čas; System.out.println ("Čas ztracený synchronizací (v mil.):" + Time_in_synchronization); System.out.println ("Uzamčení režie na hovor:" + (time_in_synchronization / ITERATIONS)); System.out.println (not_locking_time / zamykání_time * 100,0 + "% zvýšení"); }} 

Ačkoli má HotSpot VM řešit problém synchronizace a režie, HotSpot není freebee - musíte si ho koupit. Pokud s vaší aplikací nebudete licencovat a odesílat HotSpot, nelze říci, jaký virtuální počítač bude na cílové platformě, a samozřejmě chcete, aby rychlost provádění vašeho programu byla co nejmenší závislá na virtuálním počítači, který jej provádí. I když problémy se zablokováním (o kterých budu hovořit v příštím pokračování této série) neexistovaly, představa, že byste měli „vše synchronizovat“, je prostě špatně naladěná.

Souběžnost versus paralelismus

Další problém související s operačním systémem (a hlavní problém, pokud jde o psaní Java nezávislé na platformě), má co do činění s představami konkurence a rovnoběžnost. Souběžné vícevláknové systémy poskytují vzhled několika úloh prováděných najednou, ale tyto úkoly jsou ve skutečnosti rozděleny na bloky, které sdílejí procesor s bloky z jiných úkolů. Následující obrázek ilustruje problémy. V paralelních systémech se ve skutečnosti provádějí současně dva úkoly. Paralelismus vyžaduje systém s více CPU.

Pokud nebudete trávit hodně času blokováním a čekáním na dokončení operací I / O, program, který používá více souběžných vláken, bude často běžet pomaleji než ekvivalentní program s jedním vláknem, i když bude často lépe organizovaný než ekvivalentní jeden -vláknová verze. Program, který používá více vláken běžících paralelně na více procesorech, bude fungovat mnohem rychleji.

Ačkoli Java umožňuje implementaci threadingu zcela ve virtuálním počítači, alespoň teoreticky, tento přístup by vyloučil jakýkoli paralelismus ve vaší aplikaci. Pokud by nebyla použita žádná vlákna na úrovni operačního systému, operační systém by se díval na instanci virtuálního počítače jako na aplikaci s jedním vláknem, která by s největší pravděpodobností byla naplánována na jeden procesor. Čistým výsledkem by bylo, že žádné dvě podprocesy Java spuštěné pod stejnou instancí virtuálního počítače nikdy nebudou běžet paralelně, i kdybyste měli více procesorů a váš virtuální počítač byl jediným aktivním procesem. Dvě instance virtuálního počítače se samostatnými aplikacemi mohly samozřejmě běžet paralelně, ale chci to udělat lépe. Chcete-li získat paralelismus, virtuální počítač musí mapujte vlákna Java na vlákna OS; takže si nemůžete dovolit ignorovat rozdíly mezi různými modely vláken, pokud je důležitá nezávislost platformy.

Upřesněte své priority

Ukážu, jak mohou problémy, o kterých jsem právě hovořil, ovlivnit vaše programy porovnáním dvou operačních systémů: Solaris a Windows NT.

Java, alespoň teoreticky, poskytuje vláknům deset úrovní priority. (Pokud čekají na spuštění dvě nebo více vláken, spustí se ta s nejvyšší úrovní priority.) V systému Solaris, který podporuje 231 úrovní priority, to není žádný problém (i když použití priorit systému Solaris může být obtížné - více o tom za chvíli). NT má naproti tomu k dispozici sedm úrovní priority, které je třeba mapovat do deseti Java. Toto mapování není definováno, takže se nabízí spousta možností. (Například úrovně priority Java 1 a 2 se mohou mapovat na úroveň priority NT 1 a úrovně priority 8, 9 a 10 Java se mohou všechny mapovat na úroveň NT 7.)

Nedostatek NT prioritních úrovní je problém, pokud chcete použít prioritu k řízení plánování. Věci se ještě komplikují skutečností, že úrovně priorit nejsou pevně stanoveny. NT poskytuje mechanismus s názvem zvýšení priority, které můžete vypnout systémovým voláním C, ale ne z Javy. Když je povoleno zvýšení priority, NT zvýší prioritu vlákna o neurčitou částku na neurčitou dobu pokaždé, když provede určitá systémová volání související s I / O. V praxi to znamená, že úroveň priority vlákna může být vyšší, než si myslíte, protože toto vlákno náhodou provedlo I / O operaci v nepříjemné době.

Smyslem posílení priority je zabránit tomu, aby vlákna, která provádějí zpracování na pozadí, měla dopad na zjevnou odezvu úkolů náročných na uživatelské rozhraní. Jiné operační systémy mají sofistikovanější algoritmy, které obvykle snižují prioritu procesů na pozadí. Nevýhodou tohoto schématu, zejména pokud je implementováno na úrovni vlákna, a nikoli na úrovni procesu, je to, že je velmi obtížné použít prioritu k určení, kdy bude konkrétní vlákno spuštěno.

Zhoršuje se to.

V systému Solaris, stejně jako ve všech unixových systémech, mají procesy prioritu i vlákna. Vlákna procesů s vysokou prioritou nemohou být přerušena vlákny procesů s nízkou prioritou. Úroveň priority daného procesu může být navíc omezena správcem systému tak, aby uživatelský proces nepřerušoval kritické procesy operačního systému. NT nic z toho nepodporuje. Proces NT je jen adresní prostor. Nemá žádnou prioritu jako takovou a není naplánována. Systém naplánuje vlákna; potom, pokud dané vlákno běží pod procesem, který není v paměti, je proces vyměněn dovnitř. Priority podprocesů NT spadají do různých „prioritních tříd“, které jsou distribuovány v kontinuu skutečných priorit. Systém vypadá takto:

Sloupce jsou skutečné úrovně priority, pouze 22 z nich musí být sdíleny všemi aplikacemi. (Ostatní používá samotný NT.) Řádky jsou prioritní třídy. Vlákna spuštěná v procesu vázaném na třídu priorit nečinnosti běží na úrovních 1 až 6 a 15, v závislosti na jejich přiřazené úrovni logické priority. Vlákna procesu zavěšeného jako normální prioritní třída poběží na úrovních 1, 6 až 10 nebo 15, pokud proces nemá vstupní fokus. Pokud má vstupní fokus, vlákna běží na úrovních 1, 7 až 11 nebo 15. To znamená, že vlákno s vysokou prioritou procesu třídy nečinné priority může předcházet vláknu s nízkou prioritou procesu normální třídy priority, ale pouze v případě, že tento proces běží na pozadí. Všimněte si, že proces běžící ve třídě s vysokou prioritou má k dispozici pouze šest úrovní priority. Ostatní třídy mají sedm.

NT neposkytuje žádný způsob, jak omezit prioritní třídu procesu. Libovolné vlákno v jakémkoli procesu na stroji může kdykoli převzít kontrolu nad boxem posílením své vlastní prioritní třídy; proti tomu neexistuje žádná obrana.

Technický termín, který používám k popisu priority NT, je bezbožný nepořádek. V praxi je podle NT priorita prakticky bezcenná.

Co má programátor dělat? Mezi omezeným počtem prioritních úrovní NT a nekontrolovatelným zvyšováním priorit neexistuje pro program Java žádný naprosto bezpečný způsob, jak používat úrovně priorit pro plánování. Jedním proveditelným kompromisem je omezit se na Vlákno.MAX_PRIORITY, Vlákno.MIN_PRIORITY, a Vlákno.NORM_PRIORITY když zavoláš setPriority (). Toto omezení se alespoň vyhne problému 10 úrovní mapovaných na 7 úrovní. Předpokládám, že byste mohli použít název os vlastnost systému k detekci NT a poté zavolat nativní metodu k vypnutí podpory priorit, ale to nebude fungovat, pokud je vaše aplikace spuštěna v aplikaci Internet Explorer, pokud nepoužíváte také modul plug-in VM společnosti Sun. (Virtuální počítač společnosti Microsoft používá nestandardní implementaci nativní metody.) V každém případě nerad používám nativní metody. Obvykle se problému co nejvíce vyhnu tím, že na něj vložím většinu vláken NORM_PRIORITY a používání jiných než prioritních plánovacích mechanismů. (O některých z nich pojednám v budoucích částech této série.)

Spolupracovat!

Operační systémy podporují obvykle dva modely vláken: kooperativní a preventivní.

Kooperativní model multithreadingu

V družstevní systému si vlákno udrží kontrolu nad svým procesorem, dokud se ho nerozhodne vzdát (což nemusí být nikdy). Různá vlákna musí navzájem spolupracovat, nebo až na jedno vlákno budou „vyhladovělé“ (to znamená, že nikdy nedostaly šanci spustit). Plánování ve většině kooperativních systémů se provádí přísně podle úrovně priority. Když aktuální vlákno vzdá kontrolu, získá kontrolu čekající vlákno s nejvyšší prioritou. (Výjimkou z tohoto pravidla je Windows 3.x, který používá model spolupráce, ale nemá moc plánovače. Ovládací prvek získá okno, které má fokus.)

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