Vícevláknové programování ve Windows __beginThreadEx()

Ahoj,
prosím, neznáte tu někdo, jak správně používat přístup ke spouštění vláken z C++? (Windows Visual studio)
Kód níže funguje. Celý kód volám z main().
Problém, na který se potřebuju zeptat, je ten, že pokud nechám v kódu f-ci WaitForSingleObject a nechám aplikaci čekat na dokončení vlákna, aplikace po tu dobu zatuhne a čeká na vlákno.
Takto testem naspouštím např. 5 vláken a nechám je klidně počítat fibonacciho pro číslo 40. To si tedy počkám...
Prozatím spouštím kód jenom z cmd, ale chování z GUI by bylo identické.
Právě pro to jsem chtěl použít spouštění vláken a představoval jsem si, že aplikace v main() poběží dále a bude normálně reagovat na uživatelské vstupy.
Pokud už běží vlákna, aplikace nereaguje na uživatelský vstup.
Četl jsem, že f-ce WaitForSingleObject() se nemá vynechávat, že pak to prý není korektní ukončovací postup pro běžící vlákno.
Nebo se prostě vlákna nemají spouště z mainu?
Je na to nějaká finta?
Jak to udělat nejlépe?
Děkuji.

Kód: [Vybrat]
#include <process.h>
#include <Windows.h>

h = (HANDLE)_beginthreadex(0, 0, &doMyWork, &n, 0, &threadId);
threadsVector.push_back(threadId);
cout << "New thread spawned with child function. ThreadID: " << threadId << endl;
WaitForSingleObject(h, INFINITE);
if(CloseHandle(h)){
cout << "Thread finished and terminated." << endl;
}
« Poslední změna: 20. 03. 2021, 17:57:19 od Petr Krčmář »


Re:vícevláknové programování ve Windows __beginThreadEx()
« Odpověď #1 kdy: 20. 03. 2021, 15:17:25 »
Pokud vám stačí běh pouze pod Windows, ppoužijte spíše CreateThread.

Ověřujte, zda-li volání funkce skončilo úspěšně.

Úspěch volání CloseHandle nezávisí na tom, zda-li dané vlákno ještě běží či je již ukončeno. Handle je pouze jakási reference na daný objekt vlákna (trojúrovňový index do tabulky).

Ano, je slušné na běžící vlákna počkat, než se rozhodnete program ukončit. Na druhou stranu vás ale nikdo nenutí čekat, pokud program ukončit neplánujete. To je třeba případ spuštění z GUI -- chcete kus práce dělat na pozadí a hlavní vlákno nechat nezatížené, aby mohlo řešit interakci uživatele.

P.S.
Na Fib 40 nutně nemusíte čekat dlouho, pokud buď použijete nerekurentní vzorec pro jeho výpočet, nebo si pomůžete polem. Ale chápu, že se jedná o cvičení na použití více vláken.

CFM

Re:Vícevláknové programování ve Windows __beginThreadEx()
« Odpověď #2 kdy: 20. 03. 2021, 18:10:57 »
Problém, na který se potřebuju zeptat, je ten, že pokud nechám v kódu f-ci WaitForSingleObject a nechám aplikaci čekat na dokončení vlákna, aplikace po tu dobu zatuhne a čeká na vlákno.
Tady jste si vlastně odpověděl. Pokud hlavní smyčka nemá čekat, tak nečekejte a dělejte něco jiného. Stav běžícího vlákna (periodicky) kontrolujte, tak aby to vyhovovalo zamýšlené funkčnosti hlavní smyčky aplikace ...

Re:Vícevláknové programování ve Windows __beginThreadEx()
« Odpověď #3 kdy: 21. 03. 2021, 23:21:11 »
Ne nepoužívej CreateThread()! _beginthreadex() je ta správná funkce, pokud teda nechceš použít nějaké to C++11.

Podle mě je tvůj problém v WaitForSingleObject(h, INFINITE); - INFINITE - takže čekáš donekonečna...

Re:Vícevláknové programování ve Windows __beginThreadEx()
« Odpověď #4 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

Kód: [Vybrat]
#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();
}


Kód: [Vybrat]
cl.exe /EHcs test.cpp

Kód: [Vybrat]
test.exe

Kód: [Vybrat]
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.



Re:Vícevláknové programování ve Windows __beginThreadEx()
« Odpověď #5 kdy: 22. 03. 2021, 10:44:54 »
Citace
_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.

CRT může být problém i na novějších Windows; záleží, jakou verzí MSVC byl program zkompilován (jakou verzi CRT knihoven potřebuje). Ne všechny verze CRT jsou součástí OS, zejména před Windows 10. Pozor také na to, že _beginthread nemusí v určitých případech vrátit validní handle pro nové vlákno.

Citace
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.

Ano, při použití CreateThread dostanete vlákno, o kterém runtime nemusí vědět. Zejména při použití jiných programovacích jazyků se tak můžete dostat do problémů, u MS CRT jsem snad na problém nenarazil (nebo velmi okrajově).

bmn

Re:Vícevláknové programování ve Windows __beginThreadEx()
« Odpověď #6 kdy: 22. 03. 2021, 20:27:35 »
... u MS CRT jsem snad na problém nenarazil (nebo velmi okrajově).
Jakmile se reálně používá C, ne jen na hraní, tak v žádném případě nevolat CreateThread přímo, ale použít beginthread či obdobu.

Pokud se neinicializují per-thread data (a možná ani místo pro ně), což zajišťuje až ten beginthread, tak jsou problémy. Konkrétně MS CRT: Okrajové problémy jsou s exception (selhávají handlery pro signály), závažnější je pak errno a nechutně (pro debugování) může pozlobit i rand, jehož seed se udržuje per-thread. O C++ ani nemluvě. Dále předpokládám, že budou selhávat i uživatelské __declspec(thread) proměnné, rozhodně ty inicializované.

Pro C (C++) vždy jen beginthread, pokud nepíšete tak low-level, že je pro vás C jen makroassembler.

Re:Vícevláknové programování ve Windows __beginThreadEx()
« Odpověď #7 kdy: 22. 03. 2021, 20:49:05 »
Citace
Pro C (C++) vždy jen beginthread, pokud nepíšete tak low-level, že je pro vás C jen makroassembler.
Ano, u mě je to obvykle tento případ. Pokud opravdu potřebuju C/C++ runtime, chci, aby aplikace byla přenositelná, což CreateThread samo o sobě vylučuje.

Re:Vícevláknové programování ve Windows __beginThreadEx()
« Odpověď #8 kdy: 24. 03. 2021, 00:02:51 »
Citace
Ano, u mě je to obvykle tento případ. Pokud opravdu potřebuju C/C++ runtime, chci, aby aplikace byla přenositelná, což CreateThread samo o sobě vylučuje.
Hm, ze zdrojáků MS CRT to vypadá, že beginthread(ex) neprovádí žádnou extra inicializaci navíc, zejména pokud je CRT linkována dynamicky (což je asi jediný legální způsob).

V takovém případě CRT DLLka detekuje vznik či zánik vláken skrz DLL_THREAD_ATTACH a DLL_THREAD_DETACH.

Ale to je jen taková poznámka (alespoň u poslední verze CRT).

Re:Vícevláknové programování ve Windows __beginThreadEx()
« Odpověď #9 kdy: 24. 03. 2021, 00:59:30 »
Citace
zejména pokud je CRT linkována dynamicky (což je asi jediný legální způsob)

Není. Volba překladače /MT (případně /MTd) zajistí statické linkování CRT. To je důležité například když chcete distribuovat svou aplikaci jako jediné .exe bez dalších závislostí, které by se musely doinstalovávat (redist).

bmn

Re:Vícevláknové programování ve Windows __beginThreadEx()
« Odpověď #10 kdy: 24. 03. 2021, 10:02:44 »
... (alespoň u poslední verze CRT).
Zde si sypu popel na hlavu, kopíroval jsem staré poznámky, na co jsem si musel u VC dát pozor, když se mi rand (seed) choval podivně. Původ byl ze zdrojáků 11+ let starých (v novějších jsem si varování viditelně jen kopíroval, už nekontroloval). Takže nejspíše platné pro visual studio 2005 (VC 6.0). Věřím, že nověji už je to dlouho ošetřené jinak, nejspíše lépe.
« Poslední změna: 24. 03. 2021, 10:06:27 od bmn »

Re:Vícevláknové programování ve Windows __beginThreadEx()
« Odpověď #11 kdy: 24. 03. 2021, 10:22:42 »
Citace
Není. Volba překladače /MT (případně /MTd) zajistí statické linkování CRT. To je důležité například když chcete distribuovat svou aplikaci jako jediné .exe bez dalších závislostí, které by se musely doinstalovávat (redist).
Ano, ale měl jsem za to, že je tam licenční problém. Ale teď nedokážu k tomu nic moc najít.