13
« kdy: 22. 03. 2021, 02:08:24 »
Napřed si probereme, jak se dají na Windows spouštět nová vlákna.
_beginthread a _beginthreadex nejsou funkce z Windows, ale z UCRT (Universal C Run-Time library). Na Linuxu by se dalo říci, že to je LibC. UCRT je nyní součástí Windows, ale dříve to tak nebylo (tuším že WinXP), musela se dodatečně doinstalovat (takzvaný redist). Doporučuji používat, pokud chceš nová vlákna vytvářet z programovacího jazyka C.
CreateThread je funkce z Windows, již od počátku věků. Doporučuji používat, pokud chceš nová vlákna vytvářet z assembleru, nebo z jiného programovacího jazyka, který nemá run-time, nebo máš ty, jako programátor, absolutní kontrolu nad tím run-time.
std::thread je třída ze standardní knihovny C++. Doporučuji používat, pokud chceš nová vlákna vytvářet z programovacího jazyka C++.
Ty používáš C++, proč tedy nepoužívat funkce _beginthread, _beginthreadex a CreateThread? Je to kvůli tomu, že tebou zvolený programovací jazyk má tzv. run-time. Nemůžeš si jen tak vytvořit nové vlákno a v něm začít vykonávat kód v tvém programovacím jazyku aniž bys mu o tom, že jsi vytvořil nové vlákno, řekl. Není nikdo, kdo by inicializoval prostředí, které ti tvůj programovací jazyk dává per-thread. V Cčku to je například proměnná errno.
Dále si probereme, jak se (obecně) používají vlákna.
Když spustíš svůj program, operační systém ti vytvoří hlavní vlákno a spustí tzv. entry-point funkci. Ta v tvém případě inicializuje C a C++ run-time, zavolá konstruktory globálních objektů, zaregistruje spuštění destruktorů globálních objektů při vypínání programu a udělá spoustu dalších věcí, mimo jiné zavolá tvoji main funkci (nebo wmain funkci nebo WinMain funkci nebo wWinMain funkci). Dále už je to na tobě, ty můžeš vytvořit další vlákna. Pravidlo slušnosti je takové, že je někdo, kdo "vlastní" to nově spuštěné vlákno a je zodpovědný za to, že počká, až skončí. Podobně jako když alokuješ paměť pomocí new nebo malloc, nebo když otevřeš soubor pomocí fopen nebo CreateFile. Vždy máš nějaký odkaz/ukazatel/handle na ten resource co jsi alokoval (pointer, file handle, FILE* pointer, thread handle, apod.). Tento resource musí někdo uklidit, v C++ se typicky používá RAII idiom (vygůgli si to) a úklid se děje v destruktoru. To znamená, že se zavolá delete/free/CloseHandle/fclose apod. Vlákna se "uklízejí" tak, že se mu "nějak" řekne, že se má ukončit a počká se na to, až se probere, zjistí, že se má ukončit a samo se ukončí. Jsou dva způsoby programování vláken (podle mě). "Jednorázová činnost" a "smyčka".
Tvůj případ bude asi "jednorázová činnost", to jest, v hlavním vlákně si připravíš nějaký task pro tvoje nové vlákno. Data, která má zpracovat a místo, kam má odevzdat výsledek. Pak spustíš nové vlákno a jako parametr mu předáš tento task. Vlákno běží, zpracovává task a když je hotov, tak odevzdá výsledek do připraveného místa, "nějak" ohlásí, že je hotovo a ukončí se.
Smyčka je nekonečná smyčka, kdy máš nějakou datovou strukturu (frontu) tasků, které má vlákno vykonat. Spustíš vlákno, předáš mu jako parametr odkaz na tu frontu a vlákno dělá následující: Je ve frontě nějaký task? Pokud ano, tak ho vykonej, ulož výsledek, "nějak" notifikuj, že je task hotový a jdi zpět na krok 1. Pokud ne, tak "nějak" zjisti, jestli se má ukončit a ukonči se. Pokud se nemá ukončit, tak čekej na nový task a jdi spát. Hlavní vlákno (nebo jakékoli jiné) pak může do fronty přidat nový task a probudit spící vlákno.
Všimni si, že jsem několikrát použil slovo "nějak". Tím jsem myslel mezivláknovou synchronizaci, to je téma na celý samostatný článek, to sem psát nebudu, prostuduj si věci jako jsou mutex, atomic variable, critical section, promise + future, condition variable, CreateEvent, WaitForSingleObject a další.
Píšeš, že očekáváš, že se práce ve vláknu bude dít na pozadí a na popředí bude tvoje aplikace reagovat na uživatelské vstupy. Ale přitom jsi žádný takový kód nenapsal. Aby se to dělo, musel bys napsat něco ve smyslu: 1) Připrav task. 2) Spusť nové vlákno a předej mu task. 3) Je task již hotový? Pokud ne, tak zpracuj uživatelovy vstupy (třeba animuj rotující kolečko o 5°) a vrať se na bod 3. Pokud ano, tak seber výsledek a něco s ním udělej. Nastuduj si jak se ve Windows píše tzv. smyčka zpráv (používají se k tomu funkce GetMessage a DispatchMessage).
Na závěr konečně nějaký ten kód (pozor nemám tady VisualStudio IDE, mám pouze staré command-line VisualStudio Build Tools 2013). Zkopíruj tento program a ulož ho do souboru test.cpp, spusť si z nabídky Start Visual Studio developer command prompt (nebo tak nějak se to jmenuje), na command line napiš dva příkazy, jeden pro zkompilování, druhý pros spuštění nového programu.
Marek
#include <chrono>
#include <future>
#include <iostream>
#include <mutex>
#include <thread>
std::mutex g_console_mutex;
void factorial(int zadani, std::promise<int>* vysledek_promise)
{
{
std::lock_guard<std::mutex> const lck{g_console_mutex};
std::cout << "Vlakno spusteno.\n";
}
int n = zadani;
int ret = 1;
for(int i = 0; i != n; ++i)
{
ret = ret * (i + 1);
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> const lck{g_console_mutex};
std::cout << "Vlakno pocita.\n";
}
}
vysledek_promise->set_value(ret);
}
int main()
{
{
std::lock_guard<std::mutex> const lck{g_console_mutex};
std::cout << "Program spusten.\n";
}
int zadani = 5;
std::promise<int> vysledek_promise;
std::future<int> vysledek_future = vysledek_promise.get_future();
int vysledek;
std::thread vlakno{&factorial, zadani, &vysledek_promise};
for(;;)
{
std::future_status status = vysledek_future.wait_for(std::chrono::seconds(0));
if(status != std::future_status::ready)
{
{
std::lock_guard<std::mutex> const lck{g_console_mutex};
std::cout << "Program ceka, ale dela praci na popredi.\n";
}
// Tady budes obsluhovat vstupy utivatele a GUI.
std::this_thread::sleep_for(std::chrono::milliseconds(333));
continue;
}
vysledek = vysledek_future.get();
{
std::lock_guard<std::mutex> const lck{g_console_mutex};
std::cout << "Hotovo, vysledek je: " << vysledek << ".\n";
}
break;
}
// Nezapomenout pockat na ukonceni vlakna, protoze sice jiz mame vysledek,
// vlakno jiz "vypadlo" z funkce factorial, ALE jeste stale bezi.
// Vykonava se uklizeci funkce C a C++ run-time, na vlakno pockame pomoci join.
vlakno.join();
}
cl.exe /EHcs test.cpp
test.exe
Program spusten.
Program ceka, ale dela praci na popredi.
Vlakno spusteno.
Program ceka, ale dela praci na popredi.
Program ceka, ale dela praci na popredi.
Vlakno pocita.
Program ceka, ale dela praci na popredi.
Program ceka, ale dela praci na popredi.
Program ceka, ale dela praci na popredi.
Vlakno pocita.
Program ceka, ale dela praci na popredi.
Program ceka, ale dela praci na popredi.
Program ceka, ale dela praci na popredi.
Vlakno pocita.
Program ceka, ale dela praci na popredi.
Program ceka, ale dela praci na popredi.
Program ceka, ale dela praci na popredi.
Vlakno pocita.
Program ceka, ale dela praci na popredi.
Program ceka, ale dela praci na popredi.
Program ceka, ale dela praci na popredi.
Vlakno pocita.
Hotovo, vysledek je: 120.