Lekce 2 - První vícevláknová aplikace v C++
V předchozí lekci, Úvod do vícevláknových aplikací v C a C++, jsme si řekli základní pojmy k vícevláknovým aplikacím v C++, jaké knihovny budeme používat, kdy vícevláknové aplikace využijeme a další úvodní informace.
Dnes si konečně vytvoříme projekt, jak ve Visual Studiu, tak na Linuxu za použití GCC kompileru. Aplikace po spuštění nastartuje další vlákna, která budou vypisovat nějaký text. Na tomto příkladě si zároveň ukážeme, proč potřebujeme vlákna synchronizovat a jaké jsou důsledky špatně synchronizovaných vláken.
Základní program
Jak bylo řečeno v minulé lekci, budeme pracovat s knihovnou
thread
, která je určena pro C++ od standardu C++11. Tato knihovna
obsahuje třídu std::thread
, která reprezentuje vlákno. Tento
objekt přijímá jako parametr funkci, kterou má spustit v novém vlákně. Do
jisté míry je námi předaná funkce ekvivalent funkce main()
hlavního programu. Je to tedy funkce, která se začne vykonávat jako první
na novém vlákně. Ve chvíli, kdy funkce skončí (tj. funkce narazí na
příkaz return
nebo není zachycena výjimka), celé vlákno se
ukončí.
Nebudeme to dlouho zdržovat a rovnou si ukážeme náš první vícevlá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; }
Pojďme si program rozebrat. Nejprve je naimportována knihovna
iostream
(pro standardní vstup a výstup) společně s knihovnou
thread
(pro použití vláken). Stejně jako všechny ostatní
třídy ve standardní knihovně, je i třída thread
umístěna ve
jmenném prostoru std
.
Dále jsou nadefinovány dvě funkce - jedna vypisuje nuly a druhá jedničky. Všimněte si, že je použita nekonečná smyčka, funkce tedy nikdy neskončí a program budeme muset ukončit "násilnou" cestou.
Nakonec ve funkci main()
vytvoříme nové vlákno, kterému
předáme funkci vypis0()
. V tento okamžik se vlákno již může
spustit (proč může si řekneme za chvíli). Následně zavoláme
funkci vypis1()
. To znamená, že budeme mít dvě vlákna -
hlavní (to, které patří k funkci main()
a vypisuje
'1'
) a námi vytvořené (které vypisuje '0'
).
Výstup z programu může vypadat například následovně (u vás bude zcela jistě vypadat jinak).
Konzolová aplikace
00110111010111001111010111111111010111111111010000010111111111101111111111010111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000010111111111111111011111111110000000000000000000000000000000010111111111000000000000000000000000000000101111111110010111111111000000000000101111111111010111111111101011111111100000000000000101111111111000101111111110111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101111111111011111111101011111111111000000000000000000000000000000000000000000000000000000000000010111111111101111111110100000000000000000010111111111010111111111101011111111010111111111101111111100000000000101111111110111111111000000000000000000000000000000000000000000000001011111111110111111111000000000000001011111111010111111111101111111110101111111110000000000000000101111111111010111111111000000000000000000000000000000000000000000000000001111111111111111100000000000000000000000000000
Všimněte si, že jedničky a nuly nejsou úplně na střídačku, jak bychom mohli očekávat. Ze začátku je vidět, jak běží obě vlákna současně a proto se jedničky a nuly střídají relativně často. Naopak ke konci výpisu lze vidět dlouhé sekvence jedniček nebo nul, to by nám mohlo napovědět, že jedno z vláken bylo uspáno a tak do konzole vypisuje pouze druhé. Pro přesnější měření, které vlákno běží, by bylo lepší použít specializované nástroje. Problém s výpisem, jak jej máme výše, je, že výstup může být cachován (tj. uložen do mezipaměti a poté vypsán všechen naráz) a v takovém případě nemůžeme na základě výstupu soudit nic.
Kompilace
Určitě se už nemůžete dočkat, až si příklad vyzkoušíte sami. Proto se nyní podíváme na kompilaci našeho programu.
Kompilace ve Visual Studio
Ve Visual Studiu je kompilace naprosto jednoduchá. První krok je vytvořit konzolovou aplikaci standardním způsobem, viz. první konzolová aplikace). Na rozdíl od GCC je knihovna pro práci s vlákny automaticky importována do projektu. Jednoduše zkopírujte kód výše a vše by mělo fungovat.
Problém může nastat, pokud používáte předkompilované hlavičky. V
takovém případě je vypněte (klikněte pravým tlačítkem na projekt ->
Properties -> Configuration Properties -> C/C++ -> Precompiled Headers
-> Not Using Precompiled Headers) nebo na začátek souboru dopište
#include "pch.h"
pro obsažení předkompilovaných hlaviček.
Kompilace pro Linux a MacOS
Jak jsem již zmínil, pro kompilaci na platformách Linux a MacOS budeme
používat kompiler GCC. Ten byste již měli mít nainstalovaný z
předchozích kurzů. Budu předpokládat, že jste si program výše
zkopírovali a uložili do souboru vytvoreni.cpp
. Poté bude
příkaz pro kompilaci následující:
g++ -std=c++11 -pthread -o program.exe vytvoreni.cpp
Protože je knihovna thread
dostupná až od standardu C++11,
musíme specifikovat minimálně tento standard (tj. přepínač
-std
). Dále je knihovna thread
(pro platformu Linux a
MacOS) postavena nad knihovnou pthread
(neboli POSIX
threads), která je de facto standard pro UNIX-like systémy. Přepínačem
-pthread
tuto knihovnu zahrneme do cesty, aby ji linker mohl
nalézt a my mohli její funkce použít (více se dozvíte v článku o kompilaci).
Tím máme kompilaci programu probranou. Sami si můžete zkusit, jak se program bude chovat na vašem stroji s jiným hardwarem. V dalších příkladech již nebudu příkazy uvádět, protože jsou téměř totožné.
Konstruktor vlákna
Nakonec bych se ještě chtěl vrátit ke konstruktoru vlákna. Ze začátku dnešní lekce jsme si řekli, že se po volání konstruktoru vlákno může spustit (z toho logicky plyne, že nemusí). Čím je to tedy vlastně dáno? Jak jinak, než operačním systémem. Ten obsahuje tzv. scheduler (česky plánovač), který určuje jak dlouho, na kterém jádře a které vlákno poběží. Při volání konstruktoru řekneme operačnímu systému, že máme nové vlákno, které bychom chtěli spustit. O tom, kdy toto vlákno poběží, si již rozhoduje operační systém sám.
To je také hlavní důvod, proč je vícevláknové programování tak složité. My programátoři jsme zvyklí číst program shora dolů, tak, jak jsou příkazy vykonávány. To je přirozený postup, programujeme-li pro jedno vlákno. Ovšem ve chvíli, kdy máme vláken více, nemáme kontrolu nad tím, co se kdy vykonává. V takovém případě musíme počítat se všemi možnými kombinacemi (tj. každý řádek z první funkce se může vykonávat s libovolným řádkem druhé funkce). Jednoduchou matematikou zjistíme, že počet situací roste exponenciálně a promyslet si je všechny je obtížné. Jak donutit vlákna, aby mezi sebou měla alespoň nějakou synchronizaci, si řekneme v dalších lekcích.
V příští lekci, Čekání na vlákno v C++ a předávání parametrů, se podíváme na další operace s vlákny, jako je spojení a předávání parametrů, takže se určitě máte na co těšit!