Uruchamianie funkcji z kontrolą czasu

Uruchamianie funkcji z kontrolą czasu
pingwindyktator
  • Rejestracja:ponad 12 lat
  • Ostatnio:około 2 miesiące
  • Lokalizacja:Kraków
  • Postów:1055
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 **:

Kopiuj
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.
*

Kopiuj
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
*

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


do not code, write prose
edytowany 6x, ostatnio: pingwindyktator
pingwindyktator
Chyba wciąż nie działa wołanie przy edycji postu, więc wołam @kq @satirev @Endrju @Drajwer @_13th_Dragon tutaj.
KA
a ja a ja?
pingwindyktator
@karolinaa no pochwal się wiedzą jak już tu się zapędziłaś ;D
KA
no uważam, że Endrju Gynvael i Shalom mają racje
Endrju
  • Rejestracja:około 22 lata
  • Ostatnio:ponad rok
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.


"(...) otherwise, the behavior is undefined".
edytowany 1x, ostatnio: Endrju
pingwindyktator
Mogę robić detach, nie jest to problemem w moim use case.
Endrju
No i jak potem zakończysz ten wątek? Jak main wyjdzie a ten wątek dalej będzie leciał, to mogą dziać się dziwne rzeczy. (UB zapewne w którym momencie/z jakiegoś powodu)
pingwindyktator
Ten wątek zakończy się na końcu całego programu czy też nie? Bo jak nie, to problem jest.
Endrju
Mogą dziać się dziwne rzeczy. System operacyjny ostatczenie wszystko posprząta, ale wcześniej możesz pooglądać jakieś dziwne błędy.
Gynvael Coldwind
  • Rejestracja:ponad 21 lat
  • Ostatnio:około 2 miesiące
  • Lokalizacja:Zurich, Switzerland
  • Postów:457
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).


peace,
gynvael.coldwind//vx "Imagination is more important than knowledge..." Albert Einstein
Gynvael Coldwind
Dodam, że muteksy są zajmowane czasem w niekoniecznie oczywistych miejscach, typu przy inicjalizacji zmiennych typu static - "If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization." (chociaż z tego co kiedyś sprawdzałem, to kompilatory nie implementowały tego poprawnie - http://gynvael.coldwind.pl/?id=406)
Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:około 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
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.


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
pingwindyktator
  • Rejestracja:ponad 12 lat
  • Ostatnio:około 2 miesiące
  • Lokalizacja:Kraków
  • Postów:1055
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.


do not code, write prose
Gynvael Coldwind
Z tego co mi wiadomo detach() jedynie odpina faktyczny wątek od obiektu std::thread, więc wątek sobie nadal chodzi w tle. Jeśli akceptowalnym dla Ciebie jest to, że tamta funkcja nadal działa (alokuje/dealokuje pamięć, używa zasobów, etc), to deatch() faktycznie jest dobrą opcją. I tak, wątek tamten się zakończy jak funkcja zrobi return i system operacyjny po nim wyczyści wszystko.
pingwindyktator
W moim use case to nie ja powinienem przejmować się tym, że wątek w tle działa. Więc chyba to wykorzystam. Tylko jak zrobić to najsensowniej?
Gynvael Coldwind
Hmm w sumie jak napisałeś - największym problemem są te referencje w lambdzie. Przydałby Ci się jakiś dodatkowy obiekt (dynamicznie alokowany) tam, który będzie miał jakiś synchronizowany reference count i będzie trzymał wszystko co potrzebujesz. I wtedy "ostatni gasi światło" - tj. robi "opakowane w mutex reference_cnt--" na obiekcie (a konkretniej, obiekt w metodzie od zmniejszenie referencji robi delete this).
pingwindyktator
Tak spróbuję. Zawsze mogę też przekazać to jako kopia, ale to nie jest dobry pomysł zazwyczaj. Spróbuję z shared_ptr i już.
Gynvael Coldwind
Tylko pamiętaj że shared_ptr nie jest thread-safe sam w sobie. I przy okazji możesz zdradzić jak się formatuje jako kod pojedyncze słowa w postach/komentarzach :)
Gynvael Coldwind
Test: std::cout (edit: uuu działa, thx!)
kq
@Gynvael Coldwind refcount shared_ptr jest atomic. Zakładam, że chodziło Ci o obiekt przez niego wskazywany - wtedy racja, ale w przykładzie od @pingwindyktator mamy typy thread-aware (condition variable, thread, mutex...), więc tylko o synchronizację rezultatu funkcji można by się martwić.
Gynvael Coldwind
@kq: Ah, racja, przecież każdy wątek ma swoją kopię shared_ptr, a dzielony ref count jest jak piszesz, atomic. My bad ;)
Endrju
  • Rejestracja:około 22 lata
  • Ostatnio:ponad rok
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


"(...) otherwise, the behavior is undefined".
edytowany 1x, ostatnio: Endrju
pingwindyktator
Nie rozumiem. Kiedy "wątek może próbować robić coś, co już nie jest dozwolone"? Po wyjściu z main?
Gynvael Coldwind
Generalnie jeśli mnie pamięć nie myli, to po wyjściu głównego wątku (tego w main()) są wywoływane wszystkie destruktory globalne (etc), więc jeśli jakiś nieświadomy, inny wątek odwoła się do obiektu, którego destruktor już został wywołany, to może być jak @Endrju napisał (segfault, etc).
pingwindyktator
No dobra, z braku laku muszę się tym nie przejmować. Niech działa i niech będzie segfault.
Endrju
Wyobraź sobie, że wątek używa std::cout. Gdzieś po wyjściu z main wywoła się jego destrutkor, a wątek nadal będzie próbował pisać. (Zob. też 3.6.3/4)
pingwindyktator
No wiem, wiem, wszystko rozumiem.
MO
  • Rejestracja:około 10 lat
  • Ostatnio:dzień
  • Lokalizacja:Tam gdzie jest (centy)metro...
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.


Każdy problem w informatyce można rozwiązać, dodając kolejny poziom pośredniości,z wyjątkiem problemu zbyt dużej liczby warstw pośredniości — David J. Wheeler
edytowany 1x, ostatnio: Mokrowski
pingwindyktator
Dokładnie to zrobiłem w rozwiązaniu 2. I wyjaśniłem, dlaczego nie działa
MO
Ok, masz rację.... Moje tygodniowe zmęczenie... :-)
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.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.