NOVINKA – Víkendový online kurz Software tester, který tě posune dál. Zjisti, jak na to!
NOVINKA - Online rekvalifikační kurz Java programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.

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! :)


 

Předchozí článek
Úvod do vícevláknových aplikací v C a C++
Všechny články v sekci
Paralelní programování a vícevláknové aplikace v C++
Přeskočit článek
(nedoporučujeme)
Čekání na vlákno v C++ a předávání parametrů
Článek pro vás napsal Patrik Valkovič
Avatar
Uživatelské hodnocení:
11 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity