Cześć Panowie,
Przypuśćmy, że mam klasę z zdefiniowaną metodą test:
class Foo
{
public:
int test(int param1, int param2) const {
return param1 + param2;
}
};
Klasa jest oczywiście uproszczona, chodzi tylko o istotę problemu.
Klasa ta jest oddana dla klienta, czyli klient może ją użytkować jak chce. Może np wywołać ją asynchronicznie. I tu jest problem, ponieważ można to zrobić na kilka sposobów.
Nie ma problemu jeżeli, funkcję chcę wywołać np za pomocą funkcji std::async. Mogę uzyskać przyszłość reprezentującą wartość zwróconą przez funkcję. Stosujemy więc podejście oparte na zadaniach:
std::function< int(int, int) > delegateObj =
std::bind(&Foo::test, &Foo(), std::placeholders::_1, std::placeholders::_2);
auto sharedFutureObj = std::async(std::launch::async, delegateObj, 5, 5).share();
std::cout << sharedFutureObj.get() << "\n";
Problem pojawia się w momencie, kiedy chciałbym przekazać delegata bezpośrednio do obiektu klasy std::thread :
std::thread(delegateObj, 5, 5).detach();
W tym przypadku nie mogę związać uruchomienia wątku z obiektem przyszłości, więc nie mam jak odczytać wartości zwracanej. Musiałbym skorzystać z obiektu klasy std::promise<T>, ale to wiązałoby się z modyfikacją metody:
class Foo
{
public:
int test(int param1, int param2, std::promise< int >& promiseObj) const {
promiseObj.set_value(param1 + param2);
return param1 + param2;
}
};
No i dobra ok.. teraz nie ma problemu z uzyskaniem wyniku, nawet jeśli wrzucę delegata(po uprzedniej modyfikacji) prosto do konstruktora klasy std::thread :
std::function< int(int, int, std::promise< int>& ) > delegateObj =
std::bind(&Foo::test, &Foo(), std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
std::promise< int > promiseObj;
std::thread threadHandler(delegateObj, 5, 5, std::ref(promiseObj));
threadHandler.detach();
std::shared_future< int > sharedFutureObj = promiseObj.get_future();
std::cout << sharedFutureObj.get() << "\n";
No ok, teraz nie ma problemu z wywoływaniem tego delegata. Ale spójrzmy na nową deklarację metody test. Pojawia się tam typ zwracany, chociaż niczego nie przechwytujemy i pojawia się instrukcja return, której nie używamy. Jest to marnotrawstwo.
I jeżeli znowu klient będzie chciał wywołać nową metodę przy użyciu std::async, to musi przekazać adres obiektu obietnicy, którego tak naprawdę nie potrzebuje, bo funkcja zwraca instancję obiektu przyszłości. Czyli znowu marnotrawstwo.
Czy można zaimplementować metodę test tak aby klient mógł jej używać w podejściu opartym o zadania jak i w podejściu opartym o wątki? Powyższe definicje są optymalne tylko dla jednego podejścia.
Osobiście myślałem nad tym, że możnaby jakoś związać delegata i obietnicę, opakować to w jedną klasę i tak utworzony obiekt przekazać do konstruktora std::thread.
// edit
niby programiści powinni preferować podejście oparte na zadaniach, a nie to oparte na bezpośrednim wywoływaniu wątków. Chodzi chyba o to, że wywołując std::thread() przejmujemy odpowiedzialność za właściwe zarządzanie wydajnością. A wywołując std::async, odpowiedzialnośc ta spada ma implementatora biblioteki standardowej.
Ale jak już mówiłem to jest kod przeznaczony dla użytkownika zewnętrznego, więc może on robić z nim co chce.
Dobra,
Wykombinowałem to w ten sposób. A konkretnej to nie kombinowałem tylko przeciążyłem metodę, tak by zwracała void i przyjmowała referencję do obiektu std::promise<T>.
Definicja obu metod wygląda następująco:
virtual int test(int param1, int param2) const {
std::cout << "test task practise\n";
return param1 + param2;
}
virtual void test(int param1, int param2, std::promise< int >& promiseObj) const {
std::cout << "test thread practise\n";
promiseObj.set_value(param1 + param2);
}
Definicja delegatów tych funkcji wygląda następująco:
std::function< void(int, int, std::promise< int >&) > delegateObj =
std::bind(static_cast<void(Foo::*)(int, int, std::promise< int >&) const>(&Foo::test), &Foo(),
std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
std::function< int(int, int) > delegateObj2 =
std::bind(static_cast<int(Foo::*)(int, int) const>(&Foo::test), &Foo(),
std::placeholders::_1, std::placeholders::_2);
Natomiast wywołanie tych delegatów za pomocą funkcji std::async oraz obiektu std::thread, wygląda następująco:
std::promise< int > promiseObj;
std::thread threadHandler(delegateObj, 10, 8, std::ref(promiseObj));
std::shared_future< int > sharedFutureObj = promiseObj.get_future().share();
std::cout << sharedFutureObj.get() << "\n";
std::shared_future< int > sharedFutureObj2 = std::async(std::launch::async, delegateObj2, 6, 7).share();
std::cout << sharedFutureObj2.get() << "\n";
Kod działa zgodnie z oczekiwaniami. Klient może wywoływać metodę o tej samej nazwie zarówno przy użyciu podejścia opartego o zadania jak i opartego o wątki.
Problemem pozostaje jednak to, że potrzebne było przeciążenie metody. Nie wiem czy to nie jest nadmiarowość kodu i czy wgl wygląda to estetycznie (definicja delegatów to już wgl jakaś masakra jeśli idzie o czytelność, ale wygląda dosyć intuicyjnie)