Hlavní navigace

Nevtíravý paralelismus

31. 10. 2011 7:18 (aktualizováno) zboj

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.

Sdílet