Uruchamianie funkcji z kontrolą czasu

1
  • Problem **: chcę napisać wrapper na dowolną funkcję, który to

  • wykonuje tę funkcję

  • mierzy czas jej wykonania

  • pobiera zwracaną przez nią wartość

  • kończy funkcję kiedy minie określony czas a ona sama się nie skończyła. Taki timeout, odpalam funkcję, ma ona określony czas na wykonanie, jeśli nie zdąży, to przerywam jej działanie. wrapper ma skończyć działanie i zwrócić rezultat w momencie, kiedy jeden z warunków 1) funkcja skończyła działanie i zwróciła wynik 2) osiągnięto timeout zostanie spełniony

  • Założenia **: mogę zrobić std::thread::detach() na wątku wykonującym funkcję, nie mogę zrobić na nim std::terminate(), nie mam dostępu do funkcji. To jest jakaś funkcja z jakimiś argumentami, pojęcia nie mam co dzieje się w środku i nie mogę tego modyfikować. Nie chciałbym używać czegoś ponad C++11.

  • Próby rozwiązania **:

template <typename func_t, typename ... Args>
auto waiter (func_t func, const Args &... args) -> decltype(func(args...)) {
  const static std::chrono::milliseconds max_time(10);
  decltype(func(args...)) result;
  std::mutex mtx;
  std::unique_lock<std::mutex> lock(mtx);
  std::condition_variable cv;
  
    thread th([&] {
        result = func(args...);
        cv.notify_all();
    });

  auto waiting_result = cv.wait_for(lock, max_time);
  th.detach();
  if (waiting_result == std::cv_status::timeout) {
    throw std::runtime_error("timeout");
  }
  return result;
}

http://melpon.org/wandbox/permlink/4eopMBWg2RTfVtGC
niby wszystko działa dobrze, ale: po rzuceniu wyjątku std::runtime_error referencje przekazane do lambdy w std::thread::thread() są zabijane a funkcja działa dalej na nich. Mogę użyć std::shared_ptr, ale całość jest brzydka i nie dam sobie ręki uciąć, że nie ma tam 10 UB.
*

template <typename func_t, typename ... Args>
auto waiter (func_t func, const Args &... args) -> decltype(func(args...)) {
  const static std::chrono::milliseconds max_time(10);
  auto handle = std::async(std::launch::async, func, args ...);
  if (handle.wait_for(max_time) == std::future_status::timeout) {
    throw std::runtime_error("timeout");
  } else {
    return handle.get();
  }
}

fajne i ładne, natomiast nie przerywa działania po timeout, bo w std::async() tworzy się std::thread a w std::~async() jest robione coś jak std::thread::join() na tym stworzonym wątku. Głupie. http://melpon.org/wandbox/permlink/Cyd2hYJCQSIxETqL
*

template <typename func_t, typename ... Args>
auto waiter (func_t func, const Args &... args) -> decltype(func(args...)) {
  const static std::chrono::milliseconds max_time(10);
  auto task = std::packaged_task<decltype(func(args...))()>(std::bind(func, args...));
  auto handle = task.get_future();
  std::thread th(std::move(task));
  if (handle.wait_for(max_time) == std::future_status::timeout) {
  th.detach();
    throw std::runtime_error("timeout");
  } else {
    th.detach();
    return handle.get();
  }
}

to też działa http://melpon.org/wandbox/permlink/FoCdp6K8CnlmdJYi ale jestem przekonany, że robienie std::thread::detach() ma wątku z future to gwarantowane UB. Mam jednak wrażenie, że rozwiązanie poprawne jest najbliższe temu.

4

Tak na szybko, to wydaje mi się, że w samym C++11 raczej się nie uda. Nie ma niczego co pozwala zakończyć wybrany wątek jeżeli nie ma on tego mechanizmu wbudowanego. Z oczywistych względów "dowolna funkcja" nie będzie miała tego mechanizmu. Można wykombinować coś, co spowoduje że ta funkcja zostanie olana (tzn. detach) ale to nie wygląda ładnie i może spowodować problemy później (np. przy zamykaniu programu mogą dziać się dziwne rzeczy, bo wątek nie będzie poprawnie kończony).

Możesz pobrać std::thread::native_handle() i użyć funkcji z implementacji wątków (np. pthread_cancel). Oczywiście to już nie jest sam C++11 i jest zależne od implementacji.

6

Ciekawy problem :)

Zgadzam się z @Endrju, to generalnie jest trudny problem zarówno w samym C++ jak i poza nim.

Normalnie (tj. w kodzie produkcyjnym) ten problem rozwiązuje się wbudowując w funkcje mechanizmy, które pozwalają na jej przedwczesne zakończenie (takie "cancel task").

Co do tego dlaczego to jest "trudny problem":
Generalnie wraper nie wie co ta funkcja zrobiła. A nuż zajęła jakiś ważny muteks? Otworzyła 1000 plików? Zaalokowała dynamicznie 1234 MB? Etc.
Więc zabicie wykonującego ją wątku niestety (w najgorszym wypadku) może spowodować ostry resource leak i doprowadzić do deadlocków.

Dlatego też przy funkcjach typu "terminate thread" są zazwyczaj uwagi typu (WinAPI):

TerminateThread is a dangerous function that should only be used in the most extreme cases.

Więc jak wyżej - najlepiej by było jeśli byś miał możliwość obudować taką funkcję w jakiegoś rodzaju wrapper, który umiałby jej przekazać informacje, że ma przerwać działanie, co niestety nie gra z Twoim założeniem :(

Alternatywnie: Czy możesz tą funkcje wywołać w oddzielnym procesie? C++ nie ma raczej wsparcia dla procesów, natomiast to rozwiązanie ma taką zaletę, że przy zabiciu procesu wszystkie zasoby są zwalniane, więc nie musisz się martwić leakami (a system-wide mechanizmy z którymi proces-dziecko by się ew. mógł komunikować zazwyczaj są odporne na śmierć dziecka; wiesz, typu system-wide mutex czy lock na plikach).

3

Wydaje mi sie ze @Gynvael Coldwind ma tutaj rację w kontekście wykorzystania procesów zamiast wątków. Dodatkowo użycie procesów pozwala wprowadzić nawet mocniejszy sandbox np. z ograniczeniami pamięci. Ale tutaj niestety musiałbyś zdać sie na API docelowego systemu i nie byłoby to uniwersalne.

0

@Gynvael Coldwind wiem o resources leak i dlatego nie mogę twardo wywołać std::terminate(). Wolałbym też nie używać niczego oprócz C++. Ewentualnie mogę użyć nawet C++17.
Co właściwie dzieje się z funkcją, na której zrobiłem detach()? Ona w środku coś robi, nie mogę nawet założyć, że kiedykolwiek się skończy. Przy return w main ten wątek zostanie zakończony i OS po nim posprząta? Jeśli tak, to detach() jest dobrym pomysłem.

0

Zostanie zakończony. Ale po wyjściu z main program jest kończony (co się dzieje jest opisane w standardzie i również zapewne częściowo zależne od implementacji) i wątek może próbować robić coś, co już nie jest dozwolone. To może skończyć się segfaultem albo innym niespodziewanym błędem. To zależy co ta funkcja będzie robić.

Poczytaj np. tu: http://stackoverflow.com/questions/19744250/what-happens-to-a-detached-thread-when-main-exits

0

Zerknij do <future> w C++. Uruchamia zadanie i posiada wait_for() oraz wait_until().
http://en.cppreference.com/w/cpp/thread/future/wait_for

Tylko nie wiem czy dokładnie o to Ci chodziło.

2

Podsumuję to o czym rozmawialiśmy na ircu i na koniec dopiszę jeszcze jeden pomysł ; >

Odnośnie wrzuconych kodów:

  1. Jest względnie ok, poza tym, że po zawołaniu detach na th() i wywołaniu destruktora th możesz w odczepionym wątku działać na referencjach do nieistniejących obiektów. Dlatego też powinieneś w capture list przyjmować dane przez kopię [=]. Dalej zostaje kwestia odczepionego wątku (który nadal działa w tle). Wychodząc z głównego wątku podczas, gdy działa jeszcze jakiś inny wątek to UB.

  2. Tutaj masz rację, bo destruktor std::future zwróconego przez std::async czeka na zakończenie się wątku.

  3. Podobnie jak w pierwszym przypadku. Zakładając, że będziesz przyjmował dane przez kopie (wyłączywszy potencjalny UB przy wyjściu z maina) to powinno być ok. std::bind domyślnie przyjmuje dane przez kopię więc nic nie musisz zmieniać ; >

Pomimo tego, że te kody mają szanse zadziałać to żaden z nich nie będzie robił tego co chciałbyś żeby robił, tzn. detach powoduje tylko to, że *this nie jest już właścicielem wątku. Sam wątek nadal będzie wykonywany w tle. Gdybyś chciał jednak przerywać pracę wątku to musiałbyś albo ograniczyć się do wybranych platform, które udostępniają pthread_cancel albo przejść na model z procesami. O tym wszystkim wspominali już przedmówcy. Gdybyś mógł wymusić na callable cykliczne sprawdzanie interruption point to sprawa była prosta. No ale raczej nie chcesz/nie możesz przyjąć takiego założenia. W każdym razie dla zainteresowanych wspomnę, że ładne przykłady jak coś takiego zrobić (bezpiecznie) znajdziecie w książce Anthonego Williamsa C++ Concurrency in Action.

Na koniec podrzucę Ci jeszcze jeden pomysł - użycie klasy task_group z ITBB. Co prawda ITBB nie służy raczej do tego typu tasków, o których mowa w temacie ale wydaje się, że ma to szanse zadziałać dla Twojego problemu.

1 użytkowników online, w tym zalogowanych: 0, gości: 1