|
| 1 | +#pragma once |
| 2 | + |
| 3 | +#include <chrono> // std::milliseconds, std::chrono_literals |
| 4 | +#include <future> // std::future, std::promise |
| 5 | +#include <thread> // std::thread |
| 6 | +#include <type_traits> // std::invoke_result_t |
| 7 | + |
| 8 | +using namespace std::chrono_literals; |
| 9 | + |
| 10 | +namespace timeout { |
| 11 | + namespace detail { |
| 12 | + // returns true iff the given future was terminated by a timeout |
| 13 | + template <typename R> |
| 14 | + bool is_expired(std::future<R> const& future) noexcept { |
| 15 | + return future.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready; |
| 16 | + } |
| 17 | + } // namespace detail |
| 18 | + |
| 19 | + // future<void> wrapper used to signal expired timeout signals |
| 20 | + struct timeout_signal { |
| 21 | + timeout_signal() = delete; |
| 22 | + |
| 23 | + explicit timeout_signal(std::future<void>&& signal_future) : |
| 24 | + signal_future(std::move(signal_future)) { |
| 25 | + } |
| 26 | + |
| 27 | + // returns true iff the timeout expired |
| 28 | + [[nodiscard]] bool is_expired() const { |
| 29 | + return detail::is_expired(signal_future); |
| 30 | + } |
| 31 | + |
| 32 | + // create a new timeout_signal from a promise |
| 33 | + static timeout_signal from_promise(std::promise<void>& promise) { |
| 34 | + return timeout_signal{promise.get_future()}; |
| 35 | + } |
| 36 | + |
| 37 | + private: |
| 38 | + std::future<void> signal_future; |
| 39 | + }; |
| 40 | + |
| 41 | + // execute a function f with the given arguments with a timeout guard of the specified duration. |
| 42 | + // The first argument of f must be a timeout_signal&& object. |
| 43 | + // The function is executed in a separate thread. The caller thread is blocked until either |
| 44 | + // the function returns or the timeout expires, whatever of the two conditions happens earlier. |
| 45 | + // If the function concludes before the timeout expires, the result is immediately returned. |
| 46 | + // If the timeout expires while the function is still in execution, its timeout signal object is |
| 47 | + // marked as expired. The function itself must check whether the timeout signal was triggered or |
| 48 | + // not with the timeout_signal::is_expired() mmethod. |
| 49 | + template <typename Duration, typename Function, class... Args> |
| 50 | + std::invoke_result_t<Function, timeout_signal&&, Args...> with_timeout(Duration duration, |
| 51 | + Function&& f, |
| 52 | + Args&&... args) { |
| 53 | + using R = std::invoke_result_t<Function, timeout_signal&&, Args...>; |
| 54 | + |
| 55 | + // create promise and timeout for signaling the timeout expiration |
| 56 | + std::promise<void> promise_signal; |
| 57 | + timeout_signal signal{timeout_signal::from_promise(promise_signal)}; |
| 58 | + |
| 59 | + // we create a task to run the function f in a separate thread |
| 60 | + std::packaged_task<R(timeout_signal&&, Args...)> task(f); |
| 61 | + std::future<R> future = task.get_future(); |
| 62 | + |
| 63 | + // start the function in a new thread. The timeout signal object is the first argument of |
| 64 | + // the function. |
| 65 | + std::thread thread(std::move(task), std::move(signal), std::forward<Args>(args)...); |
| 66 | + |
| 67 | + // if the packaged function returns before the timeout expired, return its |
| 68 | + // result. |
| 69 | + if (future.wait_for(duration) == std::future_status::timeout) { |
| 70 | + // the allotted timeout expired. |
| 71 | + // Tell the function thread to exit the function immediately |
| 72 | + promise_signal.set_value(); |
| 73 | + } |
| 74 | + |
| 75 | + // wait for thread to join |
| 76 | + thread.join(); |
| 77 | + |
| 78 | + // return the function result |
| 79 | + return future.get(); |
| 80 | + } |
| 81 | +} // namespace timeout |
0 commit comments