Není třeba zdůrazňovat, že asynchronní provádění kódu ve více vláknech je mnohdy užitečné. Jedním z případů, kdy se velmi často používá, je síťová komunikace. Mějme tyto funkce:
std::string download(const std::string&);
void process(const std::string&);
První funkce dostane URL a vrátí jeho obsah jako řetězec. Druhá s výsledkem něco provede. Použití těchto funkcí je zřejmé.
auto s1 = download(url1);
auto s2 = download(url2);
process(s1 + s2);
Zřejmá je i nevýhoda. Funkce download čeká, až se naváže spojení a stáhnou data. Kód je tedy prováděn sekvenčně. Ideálně bychom chtěli něco jako:
auto s1 = ASYNC(download(url1));
auto s2 = ASYNC(download(url2));
process(s1 + s2);
První řádek zavolá funkci download, ale asynchronně, tj. bez zablokování aktuálního vlákna. Tím se hned provede i druhý řádek, který zahájí stahování z druhé adresy. Na třetím řádku naopak chceme aktuální vlákno zablokovat, protože pracujeme s výsledky funkce download, na něž vzhledem k relativně malé rychlosti síťové komunikace musíme počkat.
Je možné napsat v C++ takové makro? Odpověď zní ano, ale… Je třeba použít šablony, přetěžování operátorů a funktory a z C++11 auto, lambda výrazy, future a decltype. Pokud má makro fungovat i v clangu, jenž nemá lambda výrazy podle standardu, musíme mít navíc dvě verze kódu.
Samotné makro je poměrně jednoduché:
#define ASYNC(x) AsyncResult<decltype(x)>(LAMBDA(x))
Použijeme třídu AsyncResult, která dostane lambda výraz (C++11) nebo blok (clang). Od rozdílu mezi překladači nás odstíní makro LAMBDA.
#ifdef __BLOCKS__
#define LAMBDA(x) BlockWrapper<decltype(x)>(^{ return x; })
#else
#define LAMBDA(x) [=]()->decltype(x){ return x; }
#endif
Protože potřebujeme objekt typu std::function<T()>, tedy funktor, pro clang použijeme pomocnou třídu BlockWrapper, jejíž implementace je zřejmá (přetíží operátor ()). Podporuje-li překladač lambda výrazy podle standardu, použijeme přímo je.
Zbývá třída AsyncResult:
template <typename T> class AsyncResult {
private:
__std::future<T> fut;
public:
#ifdef __BLOCKS__
__AsyncResult(BlockWrapper<T>&& bw) : fut(std::async(std::launch::async, bw)) {}
#else
__AsyncResult(std::function<T()>&& f) : fut(std::async(std::launch::async, f)) {}
#endif
__AsyncResult(AsyncResult&& r) : fut(std::move(r.fut)) {}
__operator T() { return fut.get(); }
__T operator+(T n);
};
template <typename T> T AsyncResult<T>::operator+(T n) { return fut.get() + n; }
template <> std::string AsyncResult<std::string>::operator+(std::string n) {
__std::stringstream ss;
__ss << fut.get() << n;
__return ss.str();
}
Použití třídy stringstream je nutné ve Visual Studiu (v clangu funguje operátor +, ale stringstream je samozřejmě univerzální). Jak je vidět, třída AsyncResult používá std::future. Tím je umožněno asynchronní provedení kódu (přičemž konkrétní implementace je na standardní knihovně). Až výsledek potřebujeme, použije se metoda get v přetíženém operátoru + nebo přetypování (ta naopak zaručí, že aktuální vlákno počká na dokončení asynchronního kódu).
Uvedený příklad tedy spustí dvě nová vlákna, která nezávisle na sobě stáhnout obsah z URL. Zároveň je zajištěna synchronizace s aktuálním vláknem při použití výsledků. Makro ASYNC je „nevtíravé“ (nonintrusive), potřebujeme-li přeložit kód překladačem bez podpory C++11, jednoduše deklarujeme #define ASYNC(x) x.
@1 Je to nepřesně napsané, auto funguje pro -std=c++0x témeř všude, ale často chybí podpora pro <future>, lambdy apod. v STL. Pak se hodí synchronní ASYNC jako "fallback".
NB: Původní C (to před ANSI C) mělo "auto" pro deklaraci proměnných jako dědictví po B (typ odvozoval překladač později). Já si říkal, proč vymysleli "auto", když se většinou používá "var" a každý ví, o co jde. A ono to je desetiletí staré...
[8] Nehrotil bych to. Podobné věci řeší spousta knihoven a frameworků a to použití ani v C++ pak není tak strašlivé. Z kódu každopádně IMO plyne, že C++ nikdy nebude tak čitelné jako třeba Scala nebo C#, neboť je pořád ještě strašlivě ukecané a novější koncepty jsou tam tak nějak divně přilepené. Mně každopádně přijde, že se ty dva downloady provádějí sekvenčně, což je docela škoda.
@10 Provedou se paralelně. Přímé volání se nahradí lambdou ve future, buď ^{ return download(s); } nebo [=](){ return download(s); }. std::launch::async si vynutí asynchronní provedení (jinak není specifikováno). Výsledek se získá z přetíženého operátoru přetypování, interně se volá get(). Future interně využívá thread (taky z nové STL), které využívá pthreads (na Unixu, Visual Studio má svou windowsí implementaci).
Podobné věci jsme řešili před 10 lety v Javě a byla to poměrně triviální úloha. Místo typu String se udělala nová třída, která fungovala podobně jako soubor a dalo se z ní tudíž postupně načítat text. Pro operaci concatenate pak sloužila jiná třída, která uměla zabalit situaci, kdy se postupně četl zdroj 1, pak zdroj 2 atd., a to aniž by program vůbec věděl, že to je pospojovaných více zdrojů. Vše fungovalo efektivně až do chvíle, než nejmenovaný trouba do kódu propašoval řádek, který jako úplně první provedl dotaz na délku textu. To byla bohužel ta nejhorší varianta blokující metody. Používalo se to na MUDu (multi user dungeon), kde se díky tomu dokázal poskládat text pro hráče aniž by se muselo čekat, než se z databáze natáhne popisek místnosti nebo překlad textu. Jeden heartbeat takhle skládal pro každého hráče frontu textu a teprve nakonec je všechny poslal. Zatímco tedy "server" počítal boj a trávil nekonečný čas mobprogy (Java roku 2001), databázový server hledal texty.
A abych jenom nerýpal, tady je zkušební prográmek: http://pastebin.com/a5Ze23SL - přeloží se třeba pomocí
g++ --std=c++0x -lpthread strtest.cpp
ja mám tiež jeden príklad na async http://pastebin.com/Aqe4Rycj
Autor se zabývá vývojem kompilátorů a knihoven pro objektově-orientované programovací jazyky.
Přečteno 37 774×
Přečteno 26 420×
Přečteno 24 932×
Přečteno 21 278×
Přečteno 18 932×