Pro jaké účely se používají vícevláknové systémy? Vícevláknové aplikační architektury

Dřívější příspěvky hovořily o multithreadingu ve Windows pomocí CreateThread a dalších WinAPI, stejně jako o multithreadingu v Linuxu a dalších *nix systémech pomocí pthreads. Pokud píšete v C++ 11 nebo novějším, máte přístup k std::thread a dalším primitivům vláken zavedeným v daném jazykovém standardu. Dále si ukážeme, jak s nimi pracovat. Na rozdíl od WinAPI a pthreads je kód napsaný v std::thread multiplatformní.

Poznámka: Výše uvedený kód byl testován na GCC 7.1 a Clang 4.0 pod Arch Linuxem, GCC 5.4 a Clang 3.8 pod Ubuntu 16.04 LTS, GCC 5.4 a Clang 3.8 pod FreeBSD 11, stejně jako Visual Studio Community 2017 pod Windows 10. CMake nelze před verzí 3.8 mluvte s kompilátorem, abyste použili standard C++17 uvedený ve vlastnostech projektu. Jak nainstalovat CMake 3.8 na Ubuntu 16.04. Aby bylo možné kód zkompilovat pomocí Clang, musí být na systémech *nix nainstalován balíček libc++. Pro Arch Linux je balíček dostupný na AUR. Ubuntu má balíček libc++-dev, ale můžete narazit na problém, který brání snadnému sestavení kódu. Postup je popsán na StackOverflow. Na FreeBSD je ke kompilaci projektu potřeba nainstalovat balíček cmake-modules.

Mutexy

Níže je nejjednodušší příklad pomocí vláken a mutexů:

#zahrnout
#zahrnout
#zahrnout
#zahrnout

Std::mutex mtx;
statický int čítač = 0 ;


pro (;; ) (
{
std::lock_guard< std:: mutex >lock(mtx) ;

přestávka ;
int ctr_val = ++ čítač;
std::cout<< "Thread " << tnum << ": counter = " <<
ctr_val<< std:: endl ;
}

}
}

int main() (
std::vektor< std:: thread >vlákna;
for (int i = 0; i< 10 ; i++ ) {


}

// zde nelze použít const auto&, protože .join() není označeno const

thr.join();
}

Std::cout<< "Done!" << std:: endl ;
návrat 0;
}

Všimněte si zabalení std::mutex v std::lock_guard v souladu s idiomem RAII. Tento přístup zajišťuje, že mutex bude uvolněn při opuštění rozsahu v každém případě, včetně případů, kdy nastanou výjimky. Chcete-li zachytit několik mutexů najednou, abyste zabránili uváznutí, existuje třída std::scoped_lock. Objevil se však pouze v C++17, a proto nemusí fungovat všude. Pro dřívější verze C++ existuje šablona std::lock, která má podobnou funkčnost, i když pro správné uvolnění zámků pomocí RAII vyžaduje napsání dalšího kódu.

RWLock

Často nastává situace, kdy se k objektu přistupuje častěji čtením než zápisem. V tomto případě je místo běžného mutexu efektivnější použít zámek pro čtení a zápis, známý také jako RWLock. RWLock může být držen několika čtecími vlákny najednou nebo pouze jedním vláknem pro zápis. RWLock v C++ odpovídá třídám std::shared_mutex a std::shared_timed_mutex:

#zahrnout
#zahrnout
#zahrnout
#zahrnout

// std::shared_mutex mtx; // nebude fungovat s GCC 5.4
std::shared_timed_mutex mtx;

statický int čítač = 0 ;
static const int MAX_COUNTER_VAL = 100 ;

void thread_proc(int tnum) (
pro (;; ) (
{
// viz také std::shared_lock
std::unique_lock< std:: shared_timed_mutex >lock(mtx) ;
if (počítadlo == MAX_COUNTER_VAL)
přestávka ;
int ctr_val = ++ čítač;
std::cout<< "Thread " << tnum << ": counter = " <<
ctr_val<< std:: endl ;
}
std::this_thread::sleep_for(std::chrono::miliseconds(10));
}
}

int main() (
std::vektor< std:: thread >vlákna;
for (int i = 0; i< 10 ; i++ ) {
std:: thread thr(thread_proc, i) ;
threads.emplace_back(std::move(thr));
}

pro (auto & thr : vlákna) (
thr.join();
}

Std::cout<< "Done!" << std:: endl ;
návrat 0;
}

Analogicky s std::lock_guard se třídy std::unique_lock a std::shared_lock používají k zachycení RWLock v závislosti na tom, jak chceme zachytit zámek. Třída std::shared_timed_mutex se objevila v C++14 a funguje na všech* moderních platformách (nemluvě o mobilních zařízeních, herních konzolích a tak dále). Na rozdíl od std::shared_mutex má metody try_lock_for, try_lock_unti a další, které se pokoušejí uzamknout mutex v daném čase. Silně mám podezření, že std::shared_mutex musí být levnější než std::shared_timed_mutex. Nicméně std::shared_mutex se objevil pouze v C++17, což znamená, že není podporován všude. Zejména stále hojně používaný GCC 5.4 o tom neví.

Vlákno Místní úložiště

Někdy je potřeba vytvořit proměnnou, například globální, kterou však vidí pouze jedno vlákno. Ostatní vlákna také vidí proměnnou, ale pro ně má svůj vlastní lokální význam. Za tímto účelem přišli s Thread Local Storage nebo TLS (nemá nic společného s Transport Layer Security!). Pomocí TLS lze mimo jiné výrazně urychlit generování pseudonáhodných čísel. Příklad použití TLS v C++:

#zahrnout
#zahrnout
#zahrnout
#zahrnout

Std::mutex io_mtx;
thread_local int counter = 0 ;
static const int MAX_COUNTER_VAL = 10 ;

void thread_proc(int tnum) (
pro (;; ) (
čítač++ ;
if (počítadlo == MAX_COUNTER_VAL)
přestávka ;
{
std::lock_guard< std:: mutex >lock(io_mtx) ;
std::cout<< "Thread " << tnum << ": counter = " <<
čelit<< std:: endl ;
}
std::this_thread::sleep_for(std::chrono::miliseconds(10));
}
}

int main() (
std::vektor< std:: thread >vlákna;
for (int i = 0; i< 10 ; i++ ) {
std:: thread thr(thread_proc, i) ;
threads.emplace_back(std::move(thr));
}

pro (auto & thr : vlákna) (
thr.join();
}

Std::cout<< "Done!" << std:: endl ;
návrat 0;
}

Mutex zde slouží výhradně k synchronizaci výstupu do konzole. Pro přístup k proměnným thread_local není nutná žádná synchronizace.

Atomové proměnné

Atomové proměnné se často používají k provádění jednoduchých operací bez použití mutexů. Například potřebujete zvýšit čítač z více vláken. Místo zabalení int do std::mutex je efektivnější použít std::atomic_int. C++ také nabízí typy std::atomic_char, std::atomic_bool a mnoho dalších. Bezzámkové algoritmy a datové struktury jsou také implementovány pomocí atomických proměnných. Stojí za zmínku, že je velmi obtížné je vyvíjet a ladit a nepracují rychleji než podobné algoritmy a datové struktury se zámky na všech systémech.

Ukázkový kód:

#zahrnout
#zahrnout
#zahrnout
#zahrnout
#zahrnout

static std:: atomic_int atomic_counter(0) ;
static const int MAX_COUNTER_VAL = 100 ;

Std::mutex io_mtx;

void thread_proc(int tnum) (
pro (;; ) (
{
int ctr_val = ++ atomic_counter;
if (ctr_val >= MAX_COUNTER_VAL)
přestávka ;

{
std::lock_guard< std:: mutex >lock(io_mtx) ;
std::cout<< "Thread " << tnum << ": counter = " <<
ctr_val<< std:: endl ;
}
}
std::this_thread::sleep_for(std::chrono::miliseconds(10));
}
}

int main() (
std::vektor< std:: thread >vlákna;

int nthreads = std::thread::hardware_concurrency();
if (nvlákna == 0 ) nvlákna = 2 ;

for (int i = 0; i< nthreads; i++ ) {
std:: thread thr(thread_proc, i) ;
threads.emplace_back(std::move(thr));
}

pro (auto & thr : vlákna) (
thr.join();
}

Std::cout<< "Done!" << std:: endl ;
návrat 0;
}

Všimněte si použití procedury hardware_concurrency. Vrací odhadovaný počet vláken, která lze paralelně spustit na aktuálním systému. Například na stroji se čtyřjádrovým procesorem, který podporuje hyper threading, procedura vrátí číslo 8. Procedura může také vrátit nulu, pokud nelze provést vyhodnocení nebo procedura prostě není implementována.

Některé informace o fungování atomických proměnných na úrovni assembleru lze nalézt v článku Cheat Sheet pro základní x86/x64 montážní pokyny.

Závěr

Jak vidím, vše funguje opravdu dobře. To znamená, že při psaní multiplatformních aplikací v C++ můžete klidně zapomenout na WinAPI a pthreads. V čistém C jsou od C11 také multiplatformní vlákna. Ale stále nejsou podporovány sadou Visual Studio (zkontroloval jsem) a je nepravděpodobné, že budou někdy podporovány. Není žádným tajemstvím, že Microsoft nevidí zájem o rozvoj podpory jazyka C ve svém kompilátoru a raději se soustředí na C++.

V zákulisí stále zůstává mnoho primitivů: std::condition_variable(_any), std::(shared_)future, std::promise, std::sync a další. Doporučuji cppreference.com se na ně podívat. Možná by také stálo za to přečíst si knihu C++ Concurrency in Action. Musím vás ale upozornit, že už není novinka, obsahuje hodně vody a v podstatě převypráví tucet článků z cppreference.com.

Plná verze zdrojového kódu této poznámky je jako obvykle na GitHubu. Jak v současnosti píšete vícevláknové aplikace v C++?

Které téma vyvolává u začátečníků nejvíce otázek a problémů? Když jsem se na to zeptal učitele a programátora Java Alexandra Pryakhina, okamžitě odpověděl: "Multithreading." Děkujeme mu za nápad a pomoc při přípravě tohoto článku!

Nahlédneme do vnitřního světa aplikace a jejích procesů, pochopíme, co je podstatou multithreadingu, kdy je užitečný a jak jej implementovat – na příkladu Javy. Pokud se učíte další OOP jazyk, nebojte se: základní principy jsou stejné.

O proudech a jejich zdrojích

Abychom porozuměli multithreadingu, nejprve si ujasněme, co je to proces. Proces je část virtuální paměti a prostředků, které operační systém přiděluje ke spuštění programu. Pokud otevřete několik instancí jedné aplikace, systém pro každou přidělí proces. V moderních prohlížečích může být za každou kartu zodpovědný samostatný proces.

Pravděpodobně jste narazili na Windows „Správce úloh“ (v Linuxu je to „Sledování systému“) a víte, že zbytečné běžící procesy zatěžují systém a ty nejtěžší z nich často zamrzají, takže musí být násilně ukončeny.

Uživatelé však milují multitasking: nekrmte je chlebem – nechte je otevřít tucet oken a skákat tam a zpět. Nastává dilema: musíte zajistit souběžný chod aplikací a zároveň snížit zátěž systému, aby nedocházelo k jeho zpomalování. Řekněme, že hardware nedokáže držet krok s potřebami majitelů – problém je potřeba vyřešit na softwarové úrovni.

Chceme, aby procesor byl schopen provádět více příkazů a zpracovávat více dat za jednotku času. To znamená, že musíme do každého časového řezu vměstnat více provedeného kódu. Představte si jednotku provádění kódu jako objekt – toto je vlákno.

Je snazší přistoupit ke složitému úkolu, pokud jej rozdělíte na několik jednoduchých. Totéž platí při práci s pamětí: „těžký“ proces je rozdělen do vláken, která zabírají méně prostředků a doručují kód do počítače rychleji (jak přesně je uvedeno níže).

Každá aplikace má alespoň jeden proces a každý proces má alespoň jedno vlákno, které se nazývá hlavní vlákno a ze kterého se v případě potřeby spouštějí nové.

Rozdíl mezi vlákny a procesy

    Vlákna využívají paměť přidělenou procesu a procesy vyžadují samostatný prostor v paměti. Vlákna se proto vytvářejí a ukončují rychleji: systém jim nemusí pokaždé přidělovat nový adresní prostor a poté jej uvolňovat.

    Každou práci zpracovává s vlastními daty – něco si mohou vyměňovat pouze prostřednictvím mechanismu meziprocesové interakce. Vlákna si navzájem přistupují k datům a zdrojům přímo: to, co člověk změní, je okamžitě dostupné všem. Vlákno může v procesu ovládat své „bratry“, zatímco proces řídí výhradně své „dcery“. Přepínání mezi streamy je proto rychlejší a komunikace mezi nimi je snáze organizována.

Jaký z toho plyne závěr? Pokud potřebujete co nejrychleji zpracovat velké množství dat, rozdělte je na bloky, které mohou být zpracovány samostatnými vlákny, a poté dejte výsledek dohromady. To je lepší než vytvářet procesy náročné na zdroje.

Proč se ale tak populární aplikace jako Firefox vydává cestou vytváření více procesů? Protože izolovaný provoz karet je pro prohlížeč spolehlivý a flexibilní. Pokud je s jedním procesem něco v nepořádku, není nutné ukončovat celý program – je možné zachránit alespoň část dat.

Co je multithreading

Nyní se dostáváme k tomu hlavnímu. Multithreading je, když je proces aplikace rozdělen do vláken, která jsou paralelně – v jedné časové jednotce – zpracovávána procesorem.

Výpočetní zátěž je sdílena mezi dvěma nebo více jádry, aby se rozhraní a další programové komponenty vzájemně nezpomalovaly.

Vícevláknové aplikace lze spouštět i na jednojádrových procesorech, ale vlákna se pak spouštějí postupně: první fungovalo, jeho stav byl uložen - druhému bylo umožněno pracovat, vlákna byla uložena - vrátila se do první nebo spuštěný třetí atd.

Zaneprázdnění lidé si stěžují, že mají jen dvě ruce. Procesy a programy mohou mít tolik rukou, kolik je potřeba k co nejrychlejšímu dokončení úkolu.

Počkejte na signál: synchronizace ve vícevláknových aplikacích

Představte si, že se několik vláken pokouší upravit stejnou datovou oblast současně. Čí změny budou nakonec přijaty a čí změny budou zrušeny? Aby se předešlo zmatkům při práci se sdílenými prostředky, vlákna potřebují koordinovat své akce. K tomu si vyměňují informace pomocí signálů. Každé vlákno říká ostatním, co právě dělá a jaké změny lze očekávat. Tímto způsobem se synchronizují data ze všech vláken o aktuálním stavu zdrojů.

Základní synchronizační nástroje

Vzájemné vyloučení (vzájemné vyloučení, zkráceně mutex) - „příznak“, který přechází do vlákna, které má aktuálně právo pracovat se sdílenými prostředky. Zabraňuje jiným vláknům v přístupu k obsazené oblasti paměti. V aplikaci může být několik mutexů a mohou být sdíleny mezi procesy. Má to háček: mutex nutí aplikaci pokaždé přistupovat k jádru operačního systému, což je drahé.

Semafor - umožňuje omezit počet vláken přistupujících ke zdroji v daném okamžiku. Tím se sníží zatížení procesoru při provádění kódu, který má úzká hrdla. Problém je v tom, že optimální počet vláken závisí na počítači uživatele.

událost - definujete podmínku, při jejímž výskytu se řízení přenese do požadovaného vlákna. Vlákna si vyměňují data o událostech, aby se mohli rozvíjet a logicky pokračovat ve vzájemných akcích. Jeden data přijal, druhý zkontroloval jejich správnost, třetí uložil na pevný disk. Události se liší v tom, jak jsou signalizovány. Pokud potřebujete upozornit na událost několik vláken, budete muset ručně nastavit funkci zrušení pro zastavení signálu. Pokud existuje pouze jedno cílové vlákno, můžete vytvořit událost s automatickým resetem. Jakmile signál dosáhne streamu, sám to zastaví. Pro flexibilní řízení toku lze události řadit do fronty.

Kritická sekce - složitější mechanismus, který kombinuje počítadlo smyček a semafor. Počítadlo umožňuje odložit start semaforu o požadovanou dobu. Výhodou je, že jádro se používá pouze v případě, že je sekce vytížená a je potřeba zapnout semafor. Zbytek času vlákno běží v uživatelském režimu. Bohužel, sekci lze použít pouze v rámci jednoho procesu.

Jak implementovat multithreading v Javě

Třída Thread je zodpovědná za práci s vlákny v Javě. Vytvořit nové vlákno pro provedení úlohy znamená vytvořit instanci třídy Thread a přidružit ji k požadovanému kódu. To lze provést dvěma způsoby:

    podtřída Závit;

    implementujte rozhraní Runnable do vaší třídy a poté předejte instance třídy konstruktoru Thread.

I když se nebudeme dotýkat tématu patových situací, kdy si vlákna navzájem blokují práci a zamrzají, necháme si to na příští článek. Nyní přejdeme k praxi.

Příklad multithreadingu v Javě: ping pong s mutexy

Pokud si myslíte, že se stane něco hrozného, ​​vydechněte. Na práci se synchronizačními objekty se podíváme téměř herní formou: dvě vlákna se přenesou pomocí mutexu, ale v podstatě uvidíte skutečnou aplikaci, kde v jednu chvíli může zpracovávat veřejná data pouze jedno vlákno.

Nejprve vytvořte třídu, která zdědí vlastnosti nám již známého vlákna, a napište metodu „kickBall“:

Veřejná třída PingPongThread rozšiřuje vlákno( PingPongThread(název řetězce)( this.setName(název); // přepíše název vlákna ) @Override public void run() ( Ball ball = Ball.getBall(); while(ball.isInGame() ) ( kickBall(ball); ) ) private void kickBall(Ball ball) ( if(!ball.getSide().equals(getName()))( ball.kick(getName()); ) ) )

Nyní se postaráme o míč. Ta naše nebude jednoduchá, ale zapamatovatelná: aby bylo vidět, kdo ji zasáhl, z které strany a kolikrát. K tomu používáme mutex: bude shromažďovat informace o práci každého vlákna – to umožní izolovaným vláknům vzájemně komunikovat. Po 15. zásahu vyvedeme míč ze hry, abychom jej vážně nezranili.

Míč veřejné třídy ( soukromé int kopy = 0; soukromá statická instance míče = nový míč (); soukromá strana String = ""; private Ball()() statický míč getBall())( instance návratu; ) synchronizovaný void kick(String playername ) ( kicks++; side = playername; System.out.println(kopy + " " + strana); ) String getSide())( return side; ) boolean isInGame())( return (kopy< 15); } }

A nyní na scénu vstupují dvě hráčská vlákna. Nazvěme je bez dalších řečí Ping and Pong:

Veřejná třída PingPongGame ( PingPongThread player1 = new PingPongThread("Ping"); PingPongThread player2 = new PingPongThread("Pong"); Ball ball; PingPongGame())( ball = Ball.getBall(); ) void startGame() throws InterruptedException hráč1 .start(); hráč2.start(); ))

"Stadion je plný lidí - je čas začít zápas." Oznamme zahájení setkání oficiálně - v hlavní třídě aplikace:

Veřejná třída PingPong ( public static void main(String args) vyvolá InterruptedException ( hra PingPongGame = new PingPongGame(); game.startGame(); ) )

Jak vidíte, není zde nic ohromujícího. Toto je jen úvod do multithreadingu, ale už máte představu o tom, jak to funguje, a můžete experimentovat - omezovat dobu trvání hry ne počtem zásahů, ale například časem. K tématu multithreadingu se vrátíme později – podíváme se na balíček java.util.concurrent, knihovnu Akka a volatilní mechanismus. Promluvme si také o implementaci multithreadingu v Pythonu.

Vlákna a procesy jsou související pojmy ve výpočetní technice. Oba jsou posloupností instrukcí, které musí být provedeny v určitém pořadí. Instrukce v samostatných vláknech nebo procesech však mohou být prováděny paralelně.

Procesy existují v operačním systému a odpovídají tomu, co uživatelé vidí jako programy nebo aplikace. Vlákno na druhé straně existuje v procesu. Z tohoto důvodu se vlákna někdy nazývají „odlehčené procesy“. Každý proces se skládá z jednoho nebo více vláken. Existence více procesů umožňuje počítači provádět více úkolů „současně“. Existence více vláken umožňuje procesu sdílet práci pro paralelní provádění. Na víceprocesorovém počítači mohou procesy nebo vlákna běžet na různých procesorech. To umožňuje skutečně paralelní práci.

Absolutně paralelní zpracování není vždy možné. Vlákna je někdy potřeba synchronizovat. Jedno vlákno může čekat na výsledek jiného vlákna nebo jedno vlákno může potřebovat výhradní přístup k prostředku, který používá jiné vlákno. Problémy se synchronizací jsou častou příčinou chyb ve vícevláknových aplikacích. Někdy vlákno může skončit čekáním na zdroj, který nebude nikdy dostupný. To končí stavem zvaným uváznutí.

První věc, kterou se musíte naučit, je proces se skládá alespoň z jednoho vlákna. V OS má každý proces adresní prostor a jedno řídicí vlákno. Ve skutečnosti to je to, co definuje proces.

Na jedné straně proces lze chápat jako způsob spojení souvisejících zdrojů do jedné skupiny. Proces má adresní prostor obsahující programový text a data, stejně jako další zdroje. Mezi zdroje patří otevřené soubory, podřízené procesy, nezpracované poplachové zprávy, obslužné programy signálů, účetní informace a další. Je mnohem snazší spravovat zdroje jejich kombinováním ve formě procesu.

Na druhé straně, proces může být viděn jako proud spustitelných příkazů nebo jednoduše vlákno. Vlákno má programový čítač, který sleduje pořadí, ve kterém jsou prováděny akce. Má registry, které ukládají aktuální proměnné. Má zásobník obsahující protokol provádění procesu, kde je pro každou proceduru, která byla volána, ale ještě nevrátila, přidělen samostatný rámec. Ačkoli se vlákno musí spouštět v rámci procesu, je třeba rozlišovat mezi koncepty vlákna a procesu.Procesy se používají k seskupování zdrojů a vlákna jsou objekty, které se střídají ve vykonávání na CPU.

Koncept vláken dodává k procesnímu modelu schopnost současně spouštět několik programů ve stejném procesním prostředí, dostatečně nezávislý. Více vláken běžících paralelně v jednom procesu je stejné jako více procesů běžících paralelně na stejném počítači. V prvním případě vlákna sdílejí adresní prostor, otevřené soubory a další prostředky. V druhém případě procesy sdílejí fyzickou paměť, disky, tiskárny a další prostředky. Vlákna mají některé vlastnosti procesů, proto se jim někdy říká odlehčené procesy. Období multithreading používá se také k popisu použití více vláken v jednom procesu.

Žádný proud se skládá z dvě složky:

objekt jádra, přes který operační systém řídí tok. Jsou zde také uloženy statistické informace o vláknu (další vlákna jsou také vytvářena jádrem);
zásobník nití, který obsahuje parametry všech funkcí a lokálních proměnných, které vlákno potřebuje ke spuštění kódu.

Nakreslíme čáru: Hlavní rozdíl mezi procesy a vlákny, spočívá v tom, že procesy jsou od sebe izolované, takže používají různé adresní prostory a vlákna mohou používat stejný prostor (v rámci procesu) při provádění akcí, aniž by se navzájem rušily. O tom to celé je pohodlí vícevláknového programování: rozdělením aplikace do několika po sobě jdoucích vláken můžeme zvýšit výkon, zjednodušit uživatelské rozhraní a dosáhnout škálovatelnosti (pokud je vaše aplikace nainstalována na víceprocesorovém systému a spouští vlákna na různých procesorech, bude váš program pracovat úžasnou rychlostí =)).

1. Vlákno určuje pořadí provádění kódu v procesu.

2. Proces nic neprovádí, pouze slouží jako zásobník vláken.

3. Vlákna jsou vždy vytvářena v kontextu procesu a celý jejich život probíhá pouze v jeho hranicích.

4. Vlákna mohou spouštět stejný kód a manipulovat se stejnými daty a také sdílet úchyty s objekty jádra, protože tabulka úchytů se nevytváří v samostatných vláknech, ale v procesech.

5. Protože vlákna spotřebovávají podstatně méně zdrojů než procesy, snažte se vyřešit své problémy pomocí dalších vláken a vyhněte se vytváření nových procesů (ale přistupujte k tomu moudře).

Multitasking(Angličtina) multitasking) - vlastnost operačního systému nebo programovacího prostředí poskytovat možnost paralelního (nebo pseudoparalelního) zpracování několika procesů. Skutečný multitasking operačního systému je možný pouze v distribuovaných počítačových systémech.

Soubor:Snímek obrazovky Debianu (vydání 7.1, "Wheezy") s desktopovým prostředím GNOME, Firefox, Tor a VLC Player.jpg

Plocha moderního operačního systému, odrážející činnost několika procesů.

Existují 2 typy multitaskingu:

· Zpracujte multitasking(založeno na procesech – současně běžících programech). Zde je program nejmenší částí kódu, kterou lze řídit plánovačem operačního systému. Pro většinu uživatelů je známější (práce v textovém editoru a poslech hudby).

· Vláknitý multitasking(na základě vlákna). Nejmenším prvkem spravovaného kódu je vlákno (jeden program může provádět 2 nebo více úloh současně).

Multithreading je specializovaná forma multitaskingu.

· 1 Vlastnosti multitaskingového prostředí

· 2 Potíže při implementaci multitaskingového prostředí

· 3 Historie multitaskingových operačních systémů

· 4 typy pseudoparalelního multitaskingu

o 4.1 Nepreemptivní multitasking

o 4.2 Kolaborativní nebo kooperativní multitasking

o 4.3 Preemptivní nebo prioritní multitasking (v reálném čase)

· 5 Problémové situace v multitaskingových systémech

o 5.1 Hladovění

o 5.2 Podmínky závodu

· 7 poznámek

Vlastnosti multitaskingového prostředí[editovat | upravit zdrojový text]

Primitivní multitaskingová prostředí poskytují čisté „sdílení zdrojů“, kdy je každému úkolu přiřazena určitá oblast paměti a úkol je aktivován v přesně definovaných časových intervalech.

Pokročilejší multitaskingové systémy přidělují zdroje dynamicky, přičemž úloha začíná v paměti nebo opouští paměť v závislosti na její prioritě a systémové strategii. Toto multitaskingové prostředí má následující vlastnosti:

· Každá úloha má svou prioritu, podle které dostává procesorový čas a paměť

· Systém organizuje fronty úkolů tak, aby všechny úkoly obdržely zdroje v závislosti na prioritách a strategii systému

· Systém organizuje zpracování přerušení, pomocí kterého lze aktivovat, deaktivovat a mazat úkoly

· Na konci zadaného časového úseku jádro dočasně převede úlohu z běžícího stavu do připraveného stavu a poskytne zdroje jiným úlohám. Pokud není dostatek paměti, lze stránky neprovedených úloh vysunout na disk (swapping) a poté je po čase určeném systémem obnovit v paměti

· Systém chrání adresní prostor úlohy před neoprávněným zásahem jiných úloh

· Systém chrání adresní prostor svého jádra před neoprávněným zásahem úlohy

· Systém rozpozná selhání a zamrznutí jednotlivých úloh a zastaví je

· Systém řeší konflikty v přístupu ke zdrojům a zařízením, čímž se vyhne situacím uvíznutí obecného zmrazení z čekání na zablokované zdroje

· Systém každému úkolu garantuje, že dříve či později bude aktivován

· Systém zpracovává požadavky v reálném čase

· Systém zajišťuje komunikaci mezi procesy

Potíže s implementací multitaskingového prostředí[editovat | upravit zdrojový text]

Hlavním problémem při implementaci multitaskingového prostředí je jeho spolehlivost, vyjádřená ochranou paměti, zpracováním selhání a přerušení, ochranou před zamrznutím a uváznutím.

Kromě spolehlivosti musí být multitaskingové prostředí efektivní. Vynakládání prostředků na jeho údržbu by nemělo: zasahovat do procesů, zpomalovat jejich práci nebo výrazně omezovat paměť.

Vícevláknové zpracování- vlastnost platformy (například operačního systému, virtuálního stroje atd.) nebo aplikace spočívající ve skutečnosti, že proces vytvořený v operačním systému se může skládat z několika proudy, prováděné „paralelně“, tedy bez předepsaného pořadí v čase. Při plnění některých úkolů lze tímto rozdělením dosáhnout efektivnějšího využití počítačových zdrojů.

Takový proudy také zvaný vlákna provedení(z angličtiny vlákno provedení); někdy nazývané „vlákna“ (doslovný překlad angličtiny. vlákno) nebo neformálně „vlákna“.

Podstatou multithreadingu je kvazi-multitasking na úrovni jednoho spustitelného procesu, to znamená, že všechna vlákna jsou vykonávána v adresovém prostoru procesu. Kromě toho mají všechna vlákna procesu nejen společný adresní prostor, ale také společné deskriptory souborů. Běžící proces má alespoň jedno (hlavní) vlákno.

Multithreading (jako programovací doktrína) by neměl být zaměňován s multitaskingem nebo multiprocessingem, a to navzdory skutečnosti, že operační systémy, které implementují multitasking, obvykle také implementují multithreading.

Výhody multithreadingu v programování zahrnují následující:

· Zjednodušení programu v některých případech díky použití společného adresního prostoru.

· Méně času stráveného vytvářením vlákna ve srovnání s procesem.

· Zvýšení výkonu procesu paralelizací výpočtů procesoru a I/O operací.

· 1 Typy implementací vláken

· 2 Interakce vláken

· 3 Kritika terminologie

· 6 poznámek

Typy implementace vláken[upravit | upravit zdrojový text]

· Tok v uživatelském prostoru. Každý proces má tabulku vláken podobnou tabulce procesů jádra.

Výhody a nevýhody tohoto typu jsou následující: Nevýhody

1. Žádné přerušení časovače v rámci jednoho procesu

2. Když pro proces použijete blokovací systémový požadavek, zablokují se všechna jeho vlákna.

3. Složitost realizace

· Tok v prostoru jádra. Spolu s tabulkou procesů je v prostoru jádra i tabulka vláken.

· "Vlákna" vlákna). Více vláken v uživatelském režimu běžící v jednom vláknu režimu jádra. Vlákno prostoru jádra spotřebovává značné zdroje, zejména fyzickou paměť a rozsah adres režimu jádra pro zásobník režimu jádra. Proto byl představen koncept „vlákna“ - lehké vlákno, které běží výhradně v uživatelském režimu. Každé vlákno může mít více "vláken".

Interakce vláken[upravit | upravit zdrojový text]

V prostředí s více vlákny často nastávají problémy, když souběžná vlákna sdílejí stejná data nebo zařízení. K řešení takových problémů se používají metody interakce vláken, jako jsou vzájemné vyloučení (mutexy), semafory, kritické sekce a události.

· Mutex (mutex) je synchronizační objekt, který je nastaven do speciálního stavu signálu, když není obsazen žádným vláknem. V každém okamžiku vlastní tento objekt pouze jedno vlákno, odtud název těchto objektů (z angl mut obvykle např inclusive přístup - vzájemně se vylučující přístup) - současný přístup ke sdílenému zdroji je vyloučen. Po dokončení všech nezbytných akcí se mutex uvolní a umožní ostatním vláknům přístup ke sdílenému prostředku. Objekt může udržovat rekurzivní akvizici podruhé stejným vláknem, čímž zvyšuje počítadlo bez blokování vlákna a vyžaduje následné vícenásobné uvolnění. Toto je například kritická část ve Win32. Existují však také implementace, které toto nepodporují a vedou k uváznutí vlákna při pokusu o rekurzivní zachycení. Toto je FAST_MUTEX v jádře Windows.

· Semafory jsou dostupné zdroje, které lze získat více vlákny současně, dokud není fond zdrojů prázdný. Poté musí další vlákna čekat, dokud nebude znovu k dispozici požadované množství zdrojů. Semafory jsou velmi efektivní, protože umožňují současný přístup ke zdrojům. Semafor je logické rozšíření mutexu – semafor s počtem 1 je ekvivalentní mutexu, ale počet může být větší než 1.

· Události. Objekt, který ukládá 1 bit informace „signal or not“, nad kterým jsou definovány operace „signal“, „reset to unsignalized state“ a „wait“. Čekání na signalizovanou událost je nepřítomnost operace s okamžitým pokračováním provádění vlákna. Čekání na unsignaled událost způsobí, že vlákno pozastaví provádění, dokud jiné vlákno (nebo druhá fáze obsluhy přerušení v jádře OS) tuto událost nesignalizuje. V režimu „jakýkoli“ nebo „všechny“ je možné čekat na několik událostí. Je také možné vytvořit událost, která se automaticky resetuje do nesignalizovaného stavu po probuzení prvního – a jediného – čekajícího vlákna (takový objekt se používá jako základ pro implementaci objektu „kritické sekce“). Aktivně se používají v MS Windows, a to jak v uživatelském režimu, tak v režimu jádra. V linuxovém jádře existuje podobný objekt s názvem kwait_queue.

· Kritické sekce poskytují synchronizaci podobnou mutexům, kromě toho, že objekty představující kritické sekce jsou přístupné v rámci stejného procesu. Události, mutexy a semafory lze také použít v jednoprocesové aplikaci, ale implementace kritických sekcí v některých operačních systémech (jako je Windows NT) poskytují rychlejší a efektivnější vzájemně se vylučující synchronizační mechanismus – „získat“ a „uvolnit“. operace na kritické sekci jsou optimalizovány pro případ jediného vlákna (žádná konkurence), aby se zabránilo systémovým voláním vedoucím k jádru OS. Stejně jako mutexy může být objekt, který představuje kritickou sekci, používán vždy pouze jedním vláknem, což je činí extrémně užitečnými pro vymezení přístupu ke sdíleným zdrojům.

· Podmíněné proměnné (condvars). Jsou podobné událostem, ale nejsou objekty, které zabírají paměť – používá se pouze adresa proměnné, pojem „obsah proměnné“ neexistuje, adresu libovolného objektu lze použít jako podmínkovou proměnnou. Na rozdíl od událostí nemá nastavení proměnné podmínky na signalizovaný stav žádné důsledky, pokud na proměnnou aktuálně nečekají žádná vlákna. Nastavení události v podobném případě znamená uložení „signalizovaného“ stavu v samotné události, po kterém následná vlákna, která chtějí čekat na událost, pokračují v provádění okamžitě bez zastavení. K plnému využití takového objektu je také nutná operace „uvolněte mutex a počkejte na proměnnou podmínky atomicky“. Aktivně se používá v operačních systémech podobných UNIX. Diskuse o výhodách a nevýhodách událostí a stavových proměnných jsou významnou součástí diskusí o výhodách a nevýhodách Windows a UNIX.

· Port pro dokončení IO (IOCP). Implementovaný v jádře operačního systému a přístupný prostřednictvím systémových volání, objekt „queue“ s operacemi „umístit strukturu na konec fronty“ a „vzít další strukturu z hlavy fronty“ - poslední volání pozastaví provádění vlákna, pokud je fronta prázdná a dokud jiné vlákno neprovede volání "put". Nejdůležitější vlastností IOCP je, že struktury do něj mohou být umístěny nejen explicitním systémovým voláním z uživatelského režimu, ale také implicitně v jádře OS v důsledku dokončení asynchronní I/O operace na jednom z deskriptorů souborů. Chcete-li dosáhnout tohoto efektu, musíte použít systémové volání "přidružit deskriptor souboru k IOCP". V tomto případě struktura umístěná ve frontě obsahuje kód chyby I/O operace a také, pokud je tato operace úspěšná, počet skutečně zadaných nebo vydaných bajtů. Implementace portu dokončení také omezuje počet vláken spouštěných na jednom procesoru/jádru po přijetí struktury z fronty. Objekt je specifický pro MS Windows a umožňuje zpracování příchozích požadavků na připojení a datových bloků v serverovém softwaru v architektuře, kde počet vláken může být menší než počet klientů (není požadavek na vytvoření samostatného vlákna se zdrojem náklady na každého nového klienta).

· ERESOURCE. Mutex, který podporuje rekurzivní akvizici se sdílenou nebo exkluzivní sémantikou akvizice. Sémantika: objekt může být buď volný, nebo vlastněný libovolným počtem vláken sdíleným způsobem, nebo vlastněný pouze jedním vláknem výhradním způsobem. Jakékoli pokusy o uchopení, které porušují toto pravidlo, vedou k zablokování vlákna, dokud není objekt uvolněn, takže je uchopení povoleno. Existují také operace jako TryToAcquire – vlákno nikdy neblokuje, buď ho získá, nebo (pokud je zamykání potřeba) vrátí FALSE, aniž by cokoli udělal. Používá se v jádře Windows, zejména v souborových systémech - např. jakýkoli někým otevřený soubor na disku je spojen se strukturou FCB, ve které jsou 2 takové objekty pro synchronizaci přístupu k velikosti souboru. Jeden z nich, stránkovací vstupně-výstupní prostředek, je zachycen výhradně ve zkrácené cestě souboru a zajišťuje, že v souboru v době zkrácení není aktivní mezipaměť nebo vstupně-výstupní operace mapované v paměti.

· Ochrana proti vybití. Polodokumentovaný (volání jsou přítomna v hlavičkových souborech, ale ne v dokumentaci) objekt v jádře Windows. Počítadlo s operacemi „zvýšení“, „snížení“ a „čekání“. Čekání blokuje vlákno, dokud operace dekrementace nesníží čítač na nulu. Navíc může selhat operace přírůstku a aktuálně aktivní časový limit způsobí selhání všech operací přírůstku.

Clay Breshears

Úvod

Metody implementace multithreadingu společnosti Intel zahrnují čtyři hlavní fáze: analýzu, návrh a implementaci, ladění a ladění výkonu. Toto je přístup používaný k vytvoření vícevláknové aplikace ze sekvenčního kódu. Práce se softwarem při implementaci první, třetí a čtvrté etapy je pokryta poměrně široce, zatímco informace o implementaci druhého kroku zjevně nestačí.

Bylo publikováno mnoho knih o paralelních algoritmech a paralelním počítání. Tyto publikace se však zabývají především předáváním zpráv, distribuovanými paměťovými systémy nebo teoretickými paralelními výpočetními modely, které jsou někdy neaplikovatelné na skutečné vícejádrové platformy. Pokud jste připraveni brát vícevláknové programování vážně, budete pravděpodobně potřebovat znalosti o vývoji algoritmů pro tyto modely. Využití těchto modelů je samozřejmě dosti omezené, takže mnoho vývojářů softwaru je možná ještě bude muset implementovat do praxe.

Bez nadsázky lze říci, že vývoj vícevláknových aplikací je v první řadě tvůrčí a až poté vědecká činnost. V tomto článku se seznámíte s osmi jednoduchými pravidly, která vám pomohou rozšířit vaši základnu praktik paralelního programování a zlepšit efektivitu implementace vlákenného počítání ve vašich aplikacích.

Pravidlo 1. Zvýrazněte operace prováděné v kódu programu nezávisle na sobě

Paralelní zpracování je použitelné pouze pro ty operace se sekvenčním kódem, které se provádějí nezávisle na sobě. Dobrým příkladem toho, jak jednání na sobě nezávislé vedou ke skutečnému jedinému výsledku, je stavba domu. Zahrnuje pracovníky mnoha specializací: tesaře, elektrikáře, štukatéry, klempíře, pokrývače, malíře, zedníky, krajináře atd. Někteří z nich samozřejmě nemohou začít pracovat dříve, než jiní dokončí svou práci (např. pokrývači nezačnou pracovat, dokud nebudou stěny postaveny, a malíři nebudou tyto stěny natírat, pokud nebudou omítnuté). Obecně ale můžeme říci, že všichni lidé, kteří se na stavbě podílejí, jednají nezávisle na sobě.

Vezměme si další příklad – pracovní cyklus půjčovny DVD, která přijímá objednávky na určité filmy. Objednávky jsou distribuovány mezi pracovníky stanice, kteří tyto filmy hledají ve skladu. Pokud si některý z pracovníků vezme ze skladu disk, na kterém je nahrán film s Audrey Hepburnovou, samozřejmě to neovlivní dalšího pracovníka, který hledá další akční film s Arnoldem Schwarzeneggerem, a rozhodně to neovlivní jeho kolegu, který je hledám disky s novou sezónou Přátel. V našem příkladu předpokládáme, že všechny problémy s vyprodáním zásob byly vyřešeny dříve, než objednávky dorazí na místo pronájmu, a že balení a odeslání jakékoli objednávky neovlivní zpracování ostatních.

Ve své práci se pravděpodobně setkáte s výpočty, které lze zpracovávat pouze v určité posloupnosti, nikoli paralelně, jelikož různé iterace nebo kroky smyčky na sobě závisí a musí být prováděny v přísném pořadí. Vezměme si živý příklad z volné přírody. Představte si březí srnku. Vzhledem k tomu, že březost trvá v průměru osm měsíců, bez ohledu na to, jak se na to díváte, kolouch se neobjeví za měsíc, i když zabřezne osm jelenů současně. Osm sobů současně by však odvedlo skvělou práci, kdybyste je všechny zapřáhli do Santa Clausových saní.

Pravidlo 2: Aplikujte souběžnost na nízké úrovni granularity

Existují dva přístupy k paralelnímu rozdělení sekvenčního programového kódu: zdola nahoru a shora dolů. Za prvé, ve fázi analýzy kódu jsou identifikovány segmenty kódu (takzvané „horké“ body), které zabírají významnou část doby provádění programu. Oddělení těchto segmentů kódu paralelně (pokud je to možné) poskytne největší nárůst výkonu.

Přístup zdola nahoru implementuje vícevláknové zpracování aktivních bodů kódu. Pokud není možné paralelní dělení nalezených bodů, musíte prozkoumat zásobník volání aplikace a určit další segmenty, které jsou k dispozici pro paralelní dělení a běží již dlouhou dobu. Řekněme, že pracujete na aplikaci, která komprimuje grafiku. Kompresi lze implementovat pomocí několika nezávislých paralelních vláken, která zpracovávají jednotlivé segmenty obrazu. I když se vám však podaří implementovat multithreading hot spots, nezanedbávejte analýzu zásobníku volání, díky čemuž můžete najít segmenty dostupné pro paralelní dělení umístěné na vyšší úrovni programového kódu. Tímto způsobem můžete zvýšit granularitu paralelního zpracování.

V přístupu shora dolů je analyzována práce programového kódu a identifikovány jeho jednotlivé segmenty, jejichž provedení vede k dokončení celého úkolu. Pokud hlavní segmenty kódu nejsou jasně nezávislé, analyzujte jejich součásti a vyhledejte nezávislé výpočty. Analýzou kódu můžete identifikovat moduly kódu, jejichž spuštění procesoru zabere nejvíce času. Podívejme se na implementaci vláken v aplikaci určené pro kódování videa. Paralelní zpracování lze implementovat na nejnižší úrovni – pro nezávislé pixely jednoho snímku, nebo na vyšší úrovni – pro skupiny snímků, které lze zpracovávat nezávisle na ostatních skupinách. Pokud je aplikace vytvářena pro zpracování více video souborů současně, paralelní dělení na této úrovni může být ještě jednodušší a detaily budou na nejnižší úrovni.

Granularita paralelních výpočtů se týká množství výpočtů, které je nutné provést před synchronizací mezi vlákny. Jinými slovy, čím méně často dochází k synchronizaci, tím nižší je úroveň detailů. Vláknové výpočty s vysokou granularitou mohou způsobit, že systémová režie spojená s organizováním vláken překročí množství užitečných výpočtů prováděných těmito vlákny. Zvýšení počtu vláken při zachování stejného množství výpočtů komplikuje proces zpracování. Vícevláknové zpracování s nízkou granularitou způsobuje menší latenci systému a má větší potenciál pro škálovatelnost, které lze dosáhnout zavedením dalších vláken. Pro implementaci paralelního zpracování s nízkou granularitou se doporučuje použít přístup shora dolů a uspořádat vlákna na vysoké úrovni zásobníku volání.

Pravidlo 3: Zabudujte do svého kódu škálovatelnost, aby se jeho výkon zvyšoval s rostoucím počtem jader.

Není to tak dávno, co se na trhu kromě dvoujádrových procesorů objevily i čtyřjádrové. Navíc Intel již oznámil vytvoření procesoru s 80 jádry, který je schopen provádět bilion operací s pohyblivou řádovou čárkou za sekundu. Vzhledem k tomu, že počet jader v procesorech se bude časem pouze zvyšovat, musí mít váš kód odpovídající potenciál škálovatelnosti. Škálovatelnost je parametr, podle kterého lze posuzovat schopnost aplikace adekvátně reagovat na změny, jako je zvýšení systémových zdrojů (počet jader, velikost paměti, frekvence sběrnice atd.) nebo zvýšení objemu dat. Vzhledem k tomu, že počet jader v budoucích procesorech poroste, pište škálovatelný kód, který zvýší výkon díky zvýšeným systémovým zdrojům.

Abychom parafrázovali jeden ze zákonů C. Northecote Parkinsona, můžeme říci, že „zpracování dat zabírá všechny dostupné systémové zdroje“. To znamená, že s nárůstem výpočetních zdrojů (jako je počet jader) budou pro zpracování dat pravděpodobně využívány všechny. Vraťme se k výše popsané aplikaci pro kompresi videa. Vzhled dalších procesorových jader pravděpodobně neovlivní velikost zpracovávaných snímků - místo toho se zvýší počet vláken zpracovávajících snímek, což povede ke snížení počtu pixelů na vlákno. V důsledku toho se v důsledku organizace dalších vláken zvýší režie a sníží se stupeň paralelismu. Dalším pravděpodobnějším scénářem by bylo zvýšení velikosti nebo počtu video souborů, které by bylo potřeba zakódovat. V tomto případě vám organizace dalších vláken, která budou zpracovávat větší (nebo další) videosoubory, umožní rozdělit celé množství práce přímo ve fázi, kde k nárůstu došlo. Aplikace s takovými schopnostmi bude mít zase vysoký potenciál pro škálovatelnost.

Návrh a implementace paralelního zpracování pomocí dekompozice dat poskytuje zvýšenou škálovatelnost ve srovnání s použitím funkční dekompozice. Počet nezávislých funkcí v programovém kódu je nejčastěji omezen a během provádění aplikace se nemění. Vzhledem k tomu, že každé nezávislé funkci je přiděleno samostatné vlákno (a tedy jádro procesoru), pak se zvýšením počtu jader dodatečně organizovaná vlákna nezpůsobí zvýšení výkonu. Modely paralelního dělení s dekompozicí dat tedy poskytnou zvýšený potenciál pro škálovatelnost aplikací vzhledem k tomu, že s nárůstem počtu procesorových jader se zvýší objem zpracovávaných dat.

I když programový kód organizuje zpracování nezávislých funkcí ve vláknech, je pravděpodobné, že lze použít další vlákna, která se spouštějí při zvýšení zatížení vstupu. Vraťme se k výše popsanému příkladu stavby domu. Jedinečným cílem konstrukce je dokončit omezený počet nezávislých úkolů. Pokud však máte nařízeno postavit dvakrát více pater, pravděpodobně budete chtít najmout další pracovníky v některých specializacích (malíři, pokrývači, klempíři atd.). Proto musíte vyvíjet aplikace, které se dokážou přizpůsobit rozkladu dat, ke kterému dochází v důsledku zvýšené zátěže. Pokud váš kód implementuje funkční rozklad, zvažte uspořádání dalších vláken s rostoucím počtem procesorových jader.

Pravidlo 4: Používejte knihovny bezpečné pro vlákna

Pokud možná budete potřebovat knihovnu pro zpracování dat v aktivních bodech ve vašem kódu, určitě zvažte použití předpřipravených funkcí namísto vašeho vlastního kódu. Stručně řečeno, nesnažte se znovu vynalézt kolo vývojem segmentů kódu, jejichž funkčnost je již poskytována v optimalizovaných knihovních procedurách. Mnoho knihoven, včetně Intel® Math Kernel Library (Intel® MKL) a Intel® Integrated Performance Primitives (Intel® IPP), již obsahuje vícevláknové funkce optimalizované pro vícejádrové procesory.

Stojí za zmínku, že při použití procedur z vícevláknových knihoven se musíte ujistit, že volání konkrétní knihovny neovlivní normální provoz vláken. To znamená, že pokud jsou volání procedur prováděna ze dvou různých vláken, každé volání musí vrátit správné výsledky. Pokud procedury přistupují a aktualizují proměnné sdílené knihovny, může dojít k „datovému závodu“, což bude mít škodlivý vliv na spolehlivost výsledků výpočtů. Pro správnou práci s vlákny je procedura knihovny přidána jako nová (to znamená, že neaktualizuje nic jiného než lokální proměnné) nebo je synchronizována, aby byl chráněn přístup ke sdíleným zdrojům. Závěr: Před použitím jakékoli knihovny třetí strany v kódu programu si přečtěte přiloženou dokumentaci, abyste se ujistili, že funguje správně s vlákny.

Pravidlo 5: Použijte vhodný model navlékání

Řekněme, že funkce z vícevláknových knihoven zjevně nestačí na paralelní rozdělení všech relevantních segmentů kódu a museli jste přemýšlet o organizaci vláken. Pokud knihovna OpenMP již obsahuje všechny funkce, které potřebujete, nespěchejte s vytvářením vlastní (nemotorné) struktury vláken.

Nevýhodou explicitního multithreadingu je nemožnost přesně řídit vlákna.

Pokud potřebujete pouze paralelní oddělení smyček náročných na zdroje nebo je pro vás dodatečná flexibilita, kterou poskytují explicitní vlákna, druhořadá, pak v tomto případě nemá smysl dělat práci navíc. Čím složitější je implementace multithreadingu, tím větší je pravděpodobnost chyb v kódu a tím obtížnější jeho následná úprava.

Knihovna OpenMP je zaměřena na dekompozici dat a je zvláště vhodná pro zpracování smyček, které pracují s velkým množstvím informací. Navzdory skutečnosti, že pro některé aplikace je použitelný pouze rozklad dat, je nutné vzít v úvahu dodatečné požadavky (například zaměstnavatele nebo zákazníka), podle kterých je použití OpenMP nepřijatelné a zbývá implementovat multithreading pomocí explicitních metod . V tomto případě lze OpenMP použít k předvláknění k odhadu potenciálního zvýšení výkonu, škálovatelnosti a odhadovaného úsilí potřebného k následnému rozdělení kódu pomocí explicitního vlákna.

Pravidlo 6. Výsledek programového kódu by neměl záviset na sekvenci provádění paralelních vláken

Pro sekvenční kód stačí jednoduše definovat výraz, který bude proveden po jakémkoli jiném výrazu. Ve vícevláknovém kódu není pořadí spouštění vláken definováno a závisí na pokynech plánovače operačního systému. Přísně vzato je téměř nemožné předvídat sekvenci vláken, která budou spuštěna za účelem provedení jakékoli operace, nebo určit, které vlákno bude spuštěno plánovačem v následujícím okamžiku. Predikce se primárně používá ke snížení latence aplikací, zejména když běží na platformě s procesorem, který má méně jader než vlákna. Pokud je vlákno zablokováno, protože potřebuje přístup k oblasti, která není zapsána do mezipaměti, nebo protože potřebuje provést požadavek I/O operace, plánovač jej pozastaví a spustí vlákno připravené ke spuštění.

Přímým důsledkem nejistoty v plánování vláken jsou situace datového závodu. Za předpokladu, že jedno vlákno změní hodnotu sdílené proměnné dříve, než jiné vlákno tuto hodnotu přečte, může být chybné. S trochou štěstí zůstane pořadí spouštění vláken pro konkrétní platformu stejné ve všech běhech aplikace. Malé změny stavu systému (například umístění dat na pevném disku, rychlost paměti nebo dokonce odchylka od nominální frekvence střídavého proudu napájecího zdroje) však mohou vyvolat jiné pořadí spouštění vláken. U programového kódu, který správně funguje pouze s určitou posloupností vláken, jsou tedy pravděpodobné problémy spojené se situacemi závodu dat a uváznutím.

Z hlediska výkonu je vhodnější neomezovat pořadí, ve kterém jsou vlákna spouštěna. Přísná posloupnost spouštění vlákna je povolena pouze v případech krajní nutnosti, určených předem stanoveným kritériem. Pokud takové okolnosti nastanou, vlákna budou spouštěna v pořadí určeném poskytnutými synchronizačními mechanismy. Představte si například dva přátele, kteří čtou noviny, které jsou rozložené na stole. Za prvé mohou číst různou rychlostí a za druhé mohou číst různé články. A tady nezáleží na tom, kdo si přečte noviny jako první - v každém případě bude muset počkat na svého přítele, než otočí stránku. Zároveň neexistují žádná omezení na čas ani pořadí čtení článků – přátelé čtou libovolnou rychlostí a k synchronizaci mezi nimi dochází okamžitě při otočení stránky.

Pravidlo 7: Použijte místní úložiště streamu. V případě potřeby přiřaďte jednotlivým datovým oblastem zámky

Synchronizace nevyhnutelně zvyšuje zatížení systému, což v žádném případě neurychluje proces získávání výsledků paralelních výpočtů, ale zajišťuje jejich správnost. Ano, synchronizace je nutná, ale neměla by se zneužívat. Pro minimalizaci synchronizace se používá místní úložiště vláken nebo oblasti přidělené paměti (například prvky pole označené identifikátory odpovídajících vláken).

Potřeba sdílet dočasné proměnné mezi různými vlákny vzniká poměrně zřídka. Takové proměnné musí být deklarovány nebo přiděleny místně každému vláknu. Proměnné, jejichž hodnoty jsou mezivýsledky provádění vlákna, musí být také deklarovány jako místní pro odpovídající vlákna. Pro shrnutí těchto mezivýsledků v některé společné oblasti paměti bude vyžadována synchronizace. Chcete-li minimalizovat možné zatížení systému, je lepší aktualizovat tuto obecnou oblast tak zřídka, jak je to možné. Explicitní metody vícevláknového zpracování poskytují rozhraní API místního úložiště pro vlákno, která zajišťují integritu lokálních dat od začátku jednoho vícevláknového segmentu kódu k dalšímu (nebo od jednoho vícevláknového volání funkce k dalšímu provedení stejné funkce).

Pokud místní ukládání vláken není možné, je přístup ke sdíleným prostředkům synchronizován pomocí různých objektů, jako jsou zámky. Je důležité správně přiřadit zámky ke konkrétním datovým blokům, což je nejjednodušší, pokud se počet zámků rovná počtu datových bloků. Jediný zamykací mechanismus, který synchronizuje přístup k více oblastem paměti, se používá pouze v případě, že se všechny tyto oblasti nacházejí ve stejné kritické části programového kódu.

Co byste měli udělat, pokud potřebujete synchronizovat přístup k velkému množství dat, například k poli sestávajícím z 10 000 prvků? Uspořádání jediného zámku pro celé pole pravděpodobně vytvoří úzké místo v aplikaci. Opravdu musíte organizovat zamykání pro každý prvek zvlášť? Pak, i když k datům přistupuje 32 nebo 64 paralelních vláken, budete muset zabránit konfliktům přístupu k poměrně velké oblasti paměti a pravděpodobnost výskytu takových konfliktů je 1 %. Naštěstí existuje jakási zlatá střední cesta, takzvané „modulo locks“. Pokud je použito N modulo zámků, každý zámek bude synchronizovat přístup k N-té části celkové datové oblasti. Pokud jsou například uspořádány dva takové zámky, jeden z nich zabrání přístupu k sudým prvkům pole a druhý zabrání přístupu k lichým prvkům. V tomto případě vlákna, která přistupují k požadovanému prvku, určí jeho paritu a nastaví příslušný zámek. Počet modulo zámků se volí s ohledem na počet vláken a pravděpodobnost současného přístupu několika vláken do stejné oblasti paměti.

Všimněte si, že současné použití více zamykacích mechanismů není povoleno pro synchronizaci přístupu k jedné oblasti paměti. Připomeňme si Segalův zákon: „Člověk, který má jedny hodinky, přesně ví, kolik je hodin. Člověk, který má pár hodinek, si není jistý ničím.“ Předpokládejme, že přístup k proměnné je řízen dvěma různými zámky. V tomto případě může být první zámek použit jedním segmentem kódu a druhý jiným segmentem. Vlákna provádějící tyto segmenty se pak ocitnou v konfliktní situaci pro sdílená data, ke kterým současně přistupují.

Pravidlo 8. V případě potřeby změňte softwarový algoritmus pro implementaci multithreadingu

Kritériem pro hodnocení výkonu aplikací, sekvenčních i paralelních, je doba provádění. Asymptotické pořadí je vhodné jako odhad algoritmu. Pomocí tohoto teoretického ukazatele je téměř vždy možné vyhodnotit výkon aplikace. To znamená, že pokud jsou všechny ostatní věci stejné, aplikace s rychlostí růstu O(n log n) (rychlé třídění) poběží rychleji než aplikace s rychlostí růstu O(n2) (selektivní třídění), ačkoli výsledky tyto aplikace jsou stejné.

Čím lepší je asymptotické pořadí provádění, tím rychleji běží paralelní aplikace. Ani ten nejproduktivnější sekvenční algoritmus však nelze vždy rozdělit na paralelní vlákna. Pokud je rozdělení aktivního bodu programu příliš obtížné a neexistuje způsob, jak implementovat multithreading na vyšší úrovni zásobníku volání aktivního bodu, měli byste nejprve zvážit použití jiného sekvenčního algoritmu, který je snazší rozdělit než ten původní. Samozřejmě existují i ​​jiné způsoby, jak připravit programový kód pro zpracování vláken.

Pro ilustraci posledního tvrzení uvažujme násobení dvou čtvercových matic. Strassenův algoritmus má jeden z nejlepších asymptotických prováděcích příkazů: O(n2.81), který je mnohem lepší než řád O(n3) běžného algoritmu trojité vnořené smyčky. Podle Strassenova algoritmu je každá matice rozdělena do čtyř podmatic, po kterých je provedeno sedm rekurzivních volání pro vynásobení n/2 × n/2 podmatic. Chcete-li paralelizovat rekurzivní volání, můžete vytvořit nové vlákno, které bude postupně provádět sedm nezávislých násobení podmatice, dokud nedosáhnou dané velikosti. V tomto případě bude počet vláken exponenciálně narůstat a zrnitost výpočtů prováděných každým nově vytvořeným vláknem se bude zvyšovat s klesající velikostí podmatic. Zvažme další možnost – uspořádat skupinu sedmi vláken pracujících současně a každé provést jedno násobení podmatice. Když je fond vláken dokončen, je rekurzivně volána Strassenova metoda, aby se vynásobily podmatice (jako v sekvenční verzi kódu). Pokud má systém, na kterém běží takový program, více než osm procesorových jader, některá z nich budou nečinná.

Algoritmus násobení matic je mnohem jednodušší paralelizovat pomocí trojité vnořené smyčky. V tomto případě se používá dekompozice dat, kdy se matice rozdělí na řádky, sloupce nebo podmatice a každé vlákno provádí určité výpočty. Implementace takového algoritmu se provádí pomocí pragmat OpenMP vložených na některé úrovni smyčky nebo explicitním uspořádáním vláken, která provádějí dělení matic. K implementaci tohoto jednoduššího sekvenčního algoritmu bude zapotřebí mnohem méně úprav programového kódu ve srovnání s implementací vícevláknového Strassenova algoritmu.

Nyní tedy znáte osm jednoduchých pravidel pro efektivní převod sekvenčního programového kódu na paralelní. Dodržováním těchto pravidel budete mnohem rychleji vytvářet vícevláknová řešení, která mají zvýšenou spolehlivost, optimální výkon a méně úzkých míst.

Chcete-li se vrátit na webovou stránku výukových programů vícevláknového programování, přejděte na

Příklad sestavení jednoduché vícevláknové aplikace.

Born z důvodu velkého množství otázek o vytváření vícevláknových aplikací v Delphi.

Účelem tohoto příkladu je demonstrovat, jak správně sestavit vícevláknovou aplikaci, přičemž dlouhodobá práce bude přesunuta do samostatného vlákna. A jak v takové aplikaci zajistit interakci mezi hlavním vláknem a pracovním vláknem pro přenos dat z formuláře (vizuální komponenty) do vlákna a zpět.

Příklad si nečiní nárok na úplnost, pouze ukazuje nejjednodušší způsoby interakce mezi vlákny. Umožnit uživateli „rychle vytvořit“ (kdo ví, jak moc se mi to nelíbí) správně fungující vícevláknovou aplikaci.
Vše je podrobně komentováno (podle mě), ale pokud máte nějaké dotazy, ptejte se.
Ale znovu vás varuji: Streamy nejsou jednoduché. Pokud netušíte, jak to celé funguje, pak je obrovské nebezpečí, že vám často vše bude fungovat v pořádku a někdy se program bude chovat více než divně. Chování nesprávně napsaného vícevláknového programu velmi závisí na velkém množství faktorů, které někdy není možné během ladění reprodukovat.

Takže příklad. Pro usnadnění jsem zahrnul kód a připojil archiv s kódem modulu a formuláře

jednotka ExThreadForm;

používá
Windows, Zprávy, SysUtils, Varianty, Třídy, Grafika, Ovládací prvky, Formuláře,
Dialogy, StdCtrls;

// konstanty používané při přenosu dat z proudu do formuláře pomocí
// odesílání zpráv okna
konst
WM_USER_SendMessageMetod = WM_USER+10;
WM_USER_PostMessageMetod = WM_USER+11;

typ
// popis třídy vlákna, potomka třídy tThread
tMyThread = class(tThread)
soukromé
SyncDataN:Integer;
SyncDataS:String;
procedura SyncMetod1;
chráněný
postup Provést; přepsat;
veřejnost
Param1:String;
Param2:Integer;
Param3:Boolean;
Zastaveno:Boolean;
LastRandom:Integer;
IterationNo:Integer;
ResultList:tStringList;

Vytvořit konstruktor(aParam1:String);
destruktor Zničit; přepsat;
konec;

// popis třídy formuláře pomocí streamu
TForm1 = class(TForm)
Label1: TLabel;
Memo1: TMemo;
btnStart: TButton;
btnStop: TButton;
Edit1: TEdit;
Edit2: TEdit;
CheckBox1: TCheckBox;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
procedure btnStartClick(Sender: TObject);
procedure btnStopClick(Sender: TObject);
soukromé
(soukromá prohlášení)
MyThread:tMyThread;
procedure EventMyThreadOnTerminate(Sender:tObject);
procedura EventOnSendMessageMetod (var Msg: TMessage);zpráva WM_USER_SendMessageMetod;
procedure EventOnPostMessageMetod(var Msg: TMessage); zpráva WM_USER_PostMessageMetod;

Veřejnost
(Veřejná prohlášení)
konec;

var
Form1: TForm1;

{
Zastaveno – ukazuje přenos dat z formuláře do vlákna.
Nevyžaduje další synchronizaci, protože je jednoduchá
jednoslovný typ a je napsán pouze jedním vláknem.
}

procedure TForm1.btnStartClick(Sender: TObject);
začít
Randomize(); // zajištění náhodnosti v sekvenci pomocí Random() - nemá nic společného s tokem

// Vytvoří instanci objektu vlákna a předá mu vstupní parametr
{
POZORNOST!
Konstruktor vláken je napsán tak, že se vytvoří vlákno
pozastaveno, protože umožňuje:
1. Ovládejte okamžik jeho spuštění. To je téměř vždy pohodlnější, protože...
umožňuje nakonfigurovat stream ještě před spuštěním a předat mu vstup
parametry atd.
2. Protože odkaz na vytvořený objekt se pak uloží do pole formuláře
po samodestrukci vlákna (viz níže), ke které dochází při běhu vlákna
může nastat kdykoli, tento odkaz pozbude platnosti.
}
MyThread:= tMyThread.Create(Form1.Edit1.Text);

// Nicméně, protože vlákno bylo vytvořeno pozastaveno, všechny chyby
// při jeho inicializaci (před spuštěním) jej musíme sami zničit
// proč používat try / kromě bloku
Snaž se

// Přiřazení obsluhy dokončení vlákna, ve kterém obdržíme
// výsledky vlákna a „přepsat“ odkaz na něj
MyThread.OnTerminate:= EventMyThreadOnTerminate;

// Jelikož budeme brát výsledky v OnTerminate, tzn. k sebezničení
// proud, pak se zbavíme starostí s jeho zničením
MyThread.FreeOnTerminate:= True;

// Příklad předávání vstupních parametrů přes pole objektu streamu v bodě
// vytvoření instance, když ještě neběží.
// Osobně to raději dělám přes parametry overriddenu
// konstruktor (tMyThread.Create)
MyThread.Param2:= StrToInt(Form1.Edit2.Text);

MyThread.Stopped:= False; // také druh parametru, ale mění se v závislosti na
// doba běhu vlákna
až na
// protože vlákno se ještě nespustilo a nemůže se samo zničit, zničme ho "ručně"
FreeAndNil(MyThread);
// a poté nechat výjimku zpracovat jako obvykle
vyzdvihnout;
konec;

// Protože byl objekt vlákna úspěšně vytvořen a nakonfigurován, je čas jej spustit
MyThread.Resume;

ShowMessage("Stream zahájen");
konec;

procedure TForm1.btnStopClick(Sender: TObject);
začít
// Pokud instance vlákna stále existuje, požádejte ji o zastavení
// Navíc se jen „zeptáme“. V zásadě to můžeme také „vynutit“, ale bude
// výhradně nouzová možnost, která vyžaduje jasné pochopení toho všeho
// stream kuchyně. Proto se zde neuvažuje.
if Assigned(MyThread) then
MyThread.Stopped:= Pravda
jiný
ShowMessage("Vlákno neběží!");
konec;

procedure TForm1.EventOnSendMessageMetod(var Msg: TMessage);
začít
// metoda pro zpracování synchronní zprávy
// ve WParam adresa objektu tMyThread, v LParam aktuální hodnota LastRandom vlákna
s tMyThread(Msg.WParam) začněte
Form1.Label3.Caption:= Format("%d %d %d",);
konec;
konec;

procedure TForm1.EventOnPostMessageMetod(var Msg: TMessage);
začít
// metoda pro zpracování asynchronní zprávy
// ve WParam aktuální hodnota IterationNo, v LParam aktuální hodnota LastRandom vlákna
Form1.Label4.Caption:= Format("%d %d",);
konec;

procedure TForm1.EventMyThreadOnTerminate(Sender:tObject);
začít
// DŮLEŽITÉ!
// Metoda zpracování události OnTerminate je vždy volána v kontextu hlavního
// vlákno - to je zaručeno implementací tThread. Proto můžete volně
// použít libovolné vlastnosti a metody libovolných objektů

// Pro každý případ se ujistěte, že instance objektu stále existuje
pokud ne Assigned(MyThread) then Exit; // pokud tam není, tak se nedá nic dělat

// získání výsledků práce vlákna instance objektu vlákna
Form1.Memo1.Lines.Add(Format("Vlákno skončilo s výsledkem %d"));
Form1.Memo1.Lines.AddStrings((Sender as tMyThread).ResultList);

// Zničí odkaz na instanci objektu vlákna.
// Protože se naše vlákno samo ničí (FreeOnTerminate:= True)
// poté po dokončení obslužné rutiny OnTerminate bude instance objektu vlákna
// zničeno (zdarma) a všechny odkazy na něj se stanou neplatnými.
// Abyste omylem nenarazili na takový odkaz, odstraňte MyThread
// Znovu podotýkám - objekt nezničíme, ale pouze smažeme odkaz. Objekt
// se zničí!
MyThread:= Nil;
konec;

konstruktor tMyThread.Create(aParam1:String);
začít
// Vytvořte instanci vlákna SUSPENDED (viz komentář při vytváření instance)
zděděno Create(True);

// Vytvořte interní objekty (v případě potřeby)
ResultList:= tStringList.Create;

// Získání počátečních dat.

// Kopírování vstupních dat předávaných přes parametr
Param1:= aParam1;

// Příklad příjmu vstupních dat z komponent VCL v konstruktoru objektu vlákna
// To je v tomto případě přijatelné, protože konstruktor je volán v kontextu
// hlavní vlákno. Proto zde lze přistupovat ke komponentám VCL.
// Ale tohle se mi nelíbí, protože si myslím, že je špatné, když vlákno něco ví
// o nějaké formě. Ale co nemůžete udělat pro demonstraci.
Param3:= Form1.CheckBox1.Checked;
konec;

destruktor tMyThread.Destroy;
začít
// zničení vnitřních objektů
FreeAndNil(ResultList);
// zničení základny tThread
zděděno;
konec;

procedura tMyThread.Execute;
var
t:Kardinál;
s:String;
začít
IterationNo:= 0; // čítač výsledků (číslo cyklu)

// V mém příkladu je tělem vlákna smyčka, která končí
// nebo na externí „požadavek“ bude dokončen parametr Stopped procházející proměnnou,
// nebo jednoduše dokončením 5 cyklů
// Je pro mě příjemnější psát to „věčnou“ smyčkou.

Zatímco True začíná

Inc(IterationNo); // číslo dalšího cyklu

LastRandom:= Random(1000); // náhodné číslo - pro demonstraci předávání parametrů ze streamu do formuláře

T:= náhodný(5)+1; // doba, na kterou usneme, pokud nebudeme ukončeni

// Nudná operace (v závislosti na vstupním parametru)
pokud ne Param3 tak
Inc(Param2)
jiný
Dec(Param2);

// Vygenerování mezivýsledku
s:= Formát("%s %5d %s %d %d",
);

// Přidání mezivýsledku do seznamu výsledků
ResultList.Add(s);

//// Příklady předávání mezivýsledků do formuláře

//// Procházení synchronizovanou metodou - klasický způsob
//// Nedostatky:
//// - synchronizovaná metoda je obvykle metoda třídy vláken (pro přístup
//// do polí objektu stream), ale pro přístup k polím formuláře musí
//// "ví" o něm a jeho polích (objektech), s čímž to obvykle není moc dobré
//// hlediska organizace programu.
//// - aktuální vlákno bude pozastaveno, dokud nebude dokončeno provádění
//// synchronizovaná metoda.

//// Výhody:
//// - standardní a univerzální
//// - v synchronizované metodě, kterou můžete použít
//// všechna pole objektu stream.
// nejprve je v případě potřeby potřeba uložit přenášená data do
// speciální pole objektu object.
SyncDataN:= IterationNo;
SyncDataS:= "Sync"+s;
// a poté poskytněte volání synchronizované metody
Synchronize(SyncMetod1);

//// Přenos prostřednictvím synchronního odesílání zpráv (SendMessage)
//// v tomto případě lze data předat přes parametry zprávy (LastRandom),
//// a přes pole objektu předáním adresy instance v parametru zprávy
//// objekt streamu - Integer(Self).
//// Nedostatky:
//// - vlákno musí znát handle okna formuláře
//// - stejně jako u Synchronize bude aktuální vlákno pozastaveno do
//// kompletní zpracování zprávy hlavním vláknem
//// - vyžaduje značný čas CPU pro každé volání
//// (pro přepínání vláken), takže velmi časté volání je nežádoucí
//// Výhody:
//// - stejně jako u Synchronize, při zpracování zprávy můžete použít
//// všechna pole objektu streamu (pokud byla samozřejmě předána jeho adresa)


//// spusťte vlákno.
SendMessage(Form1.Handle,WM_USER_SendMessageMetod,Integer(Self),LastRandom);

//// Přenos prostřednictvím asynchronního odesílání zpráv (PostMessage)
//// Protože v tomto případě v době, kdy hlavní vlákno obdrží zprávu,
//// odesílací vlákno již mohlo být dokončeno a předalo adresu instance
//// objekt vlákna není platný!
//// Nedostatky:
//// - vlákno musí znát handle okna formuláře;
//// - díky asynchronii je přenos dat možný pouze přes parametry
//// zpráv, což výrazně komplikuje přenos dat velikosti
//// více než dvě strojová slova. Pohodlné použití pro přenos Integer atd.
//// Výhody:
//// - na rozdíl od předchozích metod aktuální vlákno NEBUDE
//// pozastaveno, ale okamžitě obnoví provádění
//// - na rozdíl od synchronizovaného volání, obsluha zpráv
//// je metoda formuláře, která musí znát objekt vlákna,
//// nebo o streamu nevím vůbec nic, pokud jsou přenášena pouze data
//// přes parametry zprávy. To znamená, že vlákno nemusí vědět nic o formuláři
//// obecně - pouze jeho Handle, které lze předat jako parametr dříve
//// spusťte vlákno.
PostMessage(Form1.Handle,WM_USER_PostMessageMetod,IterationNo,LastRandom);

//// Kontrola možného dokončení

// Kontrola dokončení podle parametru
jestliže Stopped then Break;

// Příležitostně zkontrolujte dokončení
if IterationNo >= 10 then Break;

Spánek(t*1000); // Usínejte na t sekund
konec;
konec;

procedura tMyThread.SyncMetod1;
začít
// tato metoda se volá pomocí metody Synchronize.
// To znamená, že navzdory skutečnosti, že se jedná o metodu vlákna tMyThread,
// běží v kontextu hlavního vlákna aplikace.
// Proto umí všechno, nebo skoro všechno :)
// Ale pamatujte si, že zde není třeba dlouho „dramat“.

// Předané parametry, můžeme je extrahovat ze speciálního pole, kde jsme
// uloženo před voláním.
Form1.Label1.Caption:= SyncDataS;

// nebo z jiných polí objektu flow, například odrážející jeho aktuální stav
Form1.Label2.Caption:= Format("%d %d",);
konec;

Obecně příkladu předcházely mé následující úvahy k tématu....

Za prvé:
NEJDŮLEŽITĚJŠÍ pravidlo vícevláknového programování v Delphi:
V kontextu jiného než hlavního vlákna nemáte přístup k vlastnostem a metodám formulářů a vlastně ani ke všem komponentám, které „rostou“ z tWinControl.

To znamená (poněkud zjednodušeně), že ani v metodě Execute zděděné z TThread, ani v jiných metodách/postupech/funkcích volaných z Execute, je to zakázáno nemají přímý přístup k žádným vlastnostem nebo metodám vizuálních komponent.

Jak to udělat správně.
Neexistují zde žádné běžné recepty. Přesněji řečeno, existuje tolik a různých možností, které si musíte vybrat v závislosti na konkrétním případě. Proto vás odkazují na článek. Po přečtení a pochopení bude programátor schopen pochopit, jak to v daném případě nejlépe udělat.

Ve zkratce:

Nejčastěji se aplikace stává vícevláknovou buď tehdy, když je potřeba dělat nějakou dlouhodobou práci, nebo když můžete dělat několik věcí současně, které příliš nezatěžují procesor.

V prvním případě vede implementace práce uvnitř hlavního vlákna ke „zpomalení“ uživatelského rozhraní – během práce se neprovádí smyčka zpracování zpráv. V důsledku toho program nereaguje na akce uživatele a formulář se nevykreslí například poté, co jej uživatel přesune.

Ve druhém případě, kdy práce zahrnuje aktivní výměnu s vnějším světem, pak během nuceného „prostoje“. Během čekání na příjem/odeslání dat můžete paralelně dělat něco jiného, ​​například opět odesílat/přijímat jiná data.

Existují i ​​jiné případy, ale méně časté. Na tom však nezáleží. O tom teď ne.

A teď, jak se to všechno píše? Přirozeně se uvažuje o určitém nejčastějším případě, poněkud zobecněném. Tak.

Práce prováděná v samostatném vláknu má obecně čtyři entity (nevím, jak to přesněji nazvat):
1. Počáteční údaje
2. Vlastní práce (může záviset na zdrojových datech)
3. Mezilehlé údaje (například informace o aktuálním stavu práce)
4. Výstup (výsledek)

Nejčastěji se pro čtení a zobrazení většiny dat používají vizuální komponenty. Ale, jak je uvedeno výše, nemůžete přímo přistupovat k vizuálním komponentám ze streamu. Jak být?
Vývojáři Delphi doporučují použít metodu Synchronize třídy TThread. Zde nebudu popisovat, jak jej používat - na to je výše zmíněný článek. Řeknu jen, že jeho použití, byť správné, není vždy opodstatněné. Problémy jsou dva:

Za prvé, tělo metody volané přes Synchronize se vždy spouští v kontextu hlavního vlákna, a proto se při jejím provádění opět neprovádí smyčka zpracování zpráv okna. Proto musí být proveden rychle, jinak budeme mít všechny stejné problémy jako s jednovláknovou implementací. V ideálním případě by se metoda volaná přes Synchronize měla obecně používat pouze pro přístup k vlastnostem a metodám vizuálních objektů.

Za druhé, provádění metody pomocí Synchronize je „drahý“ požitek způsobený potřebou dvou přepínačů mezi vlákny.

Oba problémy jsou navíc propojeny a způsobují rozpor: na jedné straně k vyřešení prvního je nutné metody volané přes Synchronize „skartovat“ a na druhé straně je pak třeba volat častěji, čímž ztrácíme drahocenné zdroje procesoru.

Proto jako vždy musíte přistupovat moudře a v různých případech používat různé způsoby interakce s vnějším světem:

Počáteční údaje
Všechna data, která jsou přenášena do streamu a během jeho provozu se nemění, je nutné předat ještě před jeho spuštěním, tzn. při vytváření vlákna. Chcete-li je použít v těle vlákna, musíte si vytvořit jejich místní kopii (obvykle v polích potomka TThread).
Pokud existují zdrojová data, která se mohou za běhu vlákna měnit, pak musí být přístup k takovým datům zajištěn buď prostřednictvím synchronizovaných metod (metody nazývané přes Synchronize) nebo přes pole objektu vlákna (potomek TThread). To druhé vyžaduje určitou opatrnost.

Mezilehlá a výstupní data
Zde je opět několik způsobů (v pořadí podle mých preferencí):
- Metoda pro asynchronní odesílání zpráv do hlavního okna aplikace.
Obvykle se používá k odesílání zpráv do hlavního okna aplikace o stavu procesu, přenos malého množství dat (například procento dokončení).
- Metoda pro synchronní odesílání zpráv do hlavního okna aplikace.
Obvykle se používá pro stejné účely jako asynchronní odesílání, ale umožňuje přenášet větší množství dat bez vytváření samostatné kopie.
- Synchronizované metody, kde je to možné, kombinující přenos co největšího množství dat do jedné metody.
Může být také použit pro příjem dat z formuláře.
- Prostřednictvím polí streamového objektu zajišťující vzájemně se vylučující přístup.
Více si můžete přečíst v článku.

Eh Opět to nedopadlo dobře