2. diel - Prvý viacvláknové aplikácie v C ++
V predchádzajúcej lekcii, Úvod do viacvláknových aplikácií v C a C ++ , sme si povedali základné pojmy k vícevláknovým aplikáciám v C ++, aké knižnice budeme používať, kedy viacvláknové aplikácie využijeme a ďalšie úvodné informácie. Dnes si konečne vytvoríme projekt, ako vo Visual Studiu, tak na Linuxe za použitia GCC kompileru. Aplikácia po spustení naštartuje ďalší vlákna, ktoré budú vypisovať nejaký text. Na tomto príklade si zároveň ukážeme, prečo potrebujeme vlákna synchronizovať a aké sú dôsledky zle synchronizovaných vlákien.
Základný program
Ako bolo povedané v minulej lekcii, budeme pracovať s knižnicou
thread
, ktorá je určená pre C ++ od štandardu C ++ 11. Táto
knižnica obsahuje triedu std::thread
, ktorá reprezentuje vlákno.
Tento objekt prijíma ako parameter funkciu, ktorú má spustiť v novom
vlákne. Do istej miery je nami odovzdaná funkcia ekvivalent funkcie
main()
hlavného programu. Je to teda funkcie, ktorá sa začne
vykonávať ako prvý na novom vlákne. Vo chvíli, keď funkcia skončí (tj.
Funkcie narazí na príkaz return
alebo nie je zachytená
výnimka), celé vlákno sa ukončí.
Nebudeme to dlho zdržovať a rovno si ukážeme náš prvý viacvláknové program:
#include <iostream> #include <thread> using namespace std; void vypis0() { while(true) cout.put('0'); } void vypis1() { while(true) cout.put('1'); } int main() { thread t0(vypis0); vypis1(); return 0; }
Poďme si program rozobrať. Najprv je importovaná knižnica
iostream
(pre štandardný vstup a výstup) spoločne s knižnicou
thread
(pre použitie vlákien). Rovnako ako všetky ostatné
triedy v štandardnej knižnici, je aj trieda thread
umiestnená v
mennom priestore std
.
Ďalej sú nadefinované dve funkcie - jedna vypisuje nuly a druhá jednotky. Všimnite si, že je použitá nekonečná slučka, funkcia teda nikdy neskončí a program budeme musieť ukončiť "násilnou" cestou.
Nakoniec vo funkcii main()
vytvoríme nové vlákno, ktorému
odovzdáme funkciu vypis0()
. V tento okamih sa vlákno už môže
spustiť (prečo môže si povieme za chvíľu). Následne zavoláme
funkciu vypis1()
. To znamená, že budeme mať dve vlákna -
hlavné (to, ktoré patria k funkcii main()
a vypisuje
'1'
) a nami vytvorené (ktoré vypisuje '0'
).
Výstup z programu môže vyzerať napríklad nasledovne (u vás celkom iste vyzerať inak).
Konzolová aplikácia
00110111010111001111010111111111010111111111010000010111111111101111111111010111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000010111111111111111011111111110000000000000000000000000000000010111111111000000000000000000000000000000101111111110010111111111000000000000101111111111010111111111101011111111100000000000000101111111111000101111111110111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101111111111011111111101011111111111000000000000000000000000000000000000000000000000000000000000010111111111101111111110100000000000000000010111111111010111111111101011111111010111111111101111111100000000000101111111110111111111000000000000000000000000000000000000000000000001011111111110111111111000000000000001011111111010111111111101111111110101111111110000000000000000101111111111010111111111000000000000000000000000000000000000000000000000001111111111111111100000000000000000000000000000
Všimnite si, že jednotky a nuly nie sú úplne na striedačku, ako by sme mohli očakávať. Zo začiatku je vidieť, ako bežia obe vlákna súčasne a preto sa jednotky a nuly striedajú relatívne často. Naopak ku koncu výpisu možno vidieť dlhé sekvencie jedničiek alebo núl, to by nám mohlo napovedať, že jedno z vlákien bolo uspanie a tak do konzoly vypisuje iba druhé. Pre presnejšie meranie, ktoré vlákno beží, by bolo lepšie použiť špecializované nástroje. Problém s výpisom, ako ho máme vyššie, je, že výstup môže byť cachovanie (tj. Uložený do medzipamäte a potom vypísaný všetok naraz) a v takom prípade nemôžeme na základe výstupe súdiť nič.
Kompilácie
Určite sa už nemôžete dočkať, až si príklad vyskúšate sami. Preto sa teraz pozrieme na kompilácii nášho programu.
Kompilácie vo Visual Studio
Vo Visual Studiu je kompilácia úplne jednoduchá. Prvý krok je vytvoriť konzolovú aplikáciu štandardným spôsobom, viď. prvý konzolová aplikácie). Na rozdiel od GCC je knižnica pre prácu s vláknami automaticky importovaná do projektu. Jednoducho skopírujte kód vyššie a všetko by malo fungovať.
Problém môže nastať, ak používate predkompilované hlavičky. V takom
prípade je vypnite (kliknite pravým tlačidlom na projekt -> Properties
-> Configuration Properties -> C / C ++ -> Precompiled Headers ->
Not Using Precompiled Headers) alebo na začiatok súboru dopíšte
#include "pch.h"
pre obsiahnutie předkompilovaných hlavičiek
.
Kompilácie pre Linux a MacOS
Ako som už spomenul, pre kompiláciu na platformách Linux a MacOS budeme
používať kompiler GCC. Ten by ste už mali mať nainštalovaný z
predchádzajúcich kurzov. Budem predpokladať, že ste si program vyššie
skopírovali a uložili do súboru vytvoreni.cpp
. Potom bude
príkaz pre kompiláciu nasledujúce:
g++ -std=c++11 -pthread -o program.exe vytvoreni.cpp
Pretože je knižnica thread
dostupná až od štandardu C ++
11, musíme špecifikovať minimálne tento štandard (tj. Prepínač
-std
). Ďalej je knižnica thread
(pre platformu Linux
a MacOS) postavená nad knižnicou pthread
(čiže POSIX
threads), ktorá je de facto štandard pre UNIX-like systémy. Prepínačom
-pthread
túto knižnicu zahrnieme do cesty, aby ju linker mohol
nájsť a my mohli jej funkcie použiť (viac sa dozviete v článku o kompiláciu).
Tým máme kompiláciu programu prebranú. Sami si môžete skúsiť, ako sa program bude správať na vašom stroji s iným hardvérom. V ďalších príkladoch už nebudem príkazy uvádzať, pretože sú takmer totožné.
Konštruktor vlákna
Nakoniec by som sa ešte chcel vrátiť ku konstruktoru vlákna. Zo začiatku dnešnej lekcie sme si povedali, že sa po volaní konstruktoru vlákno môže spustiť (z toho logicky vyplýva, že nemusí). Čím je to teda vlastne dané? Ako inak, než operačným systémom. Ten obsahuje tzv. Scheduler (slovensky plánovač), ktorý určuje ako dlho, na ktorom jadre a ktoré vlákno pobeží. Pri volaní konstruktoru povieme operačnému systému, že máme nové vlákno, ktoré by sme chceli spustiť. O tom, kedy toto vlákno pobeží, si už rozhoduje operačný systém sám.
To je tiež hlavný dôvod, prečo je viacvláknové programovanie tak zložité. My programátori sme zvyknutí čítať program zhora nadol, tak, ako sú príkazy vykonávané. To je prirodzený postup, programujeme ak pre jedno vlákno. Avšak vo chvíli, kedy máme vlákien viac, nemáme kontrolu nad tým, čo sa kedy vykonáva. V takom prípade musíme počítať so všetkými možnými kombináciami (tj. Každý riadok z prvej funkcie sa môže vykonávať s ľubovoľným riadkom druhej funkcie). Jednoduchou matematikou zistíme, že počet situácií rastie exponenciálne a premyslieť si ich všetky je ťažké. Ako donútiť vlákna, aby medzi sebou mala aspoň nejakú synchronizáciu, si povieme v ďalších lekciách.
V budúcej lekcii, Čakanie na vlákno v C ++ a odovzdávanie parametrov , sa pozrieme na ďalšie operácie s vláknami, ako je spojenie a odovzdávanie parametrov, takže sa určite máte na čo tešiť!:)