Home > Blockchain >  C function timeout wrapper
C function timeout wrapper

Time:02-15

Based on How to implement timeout for function in c , I wrote this wrapper:

template <typename t_time, typename t_function, typename... t_params>
inline std::conditional_t<
    // if 't_function' return type is 'void'
    std::is_void_v<std::invoke_result_t<t_function, const bool &, t_params...>>,
    // the 'execute' wrapper will return 'bool', which will be 'true' if the
    // 'p_function' executes in less 'p_max_time', or 'false' otherwise
    bool,
    // else it will result a 'std::optional' with the return type of
    // 't_function', which will contain a value of that type, if the
    // 'p_function' executes in less 'p_max_time', or empty otherwise
    std::optional<std::invoke_result_t<t_function, const bool &, t_params...>>>
execute(t_time p_max_time, t_function &p_function, t_params &&... p_params) {
  std::mutex _mutex;
  std::condition_variable _cond;
  bool _timeout{false};

  typedef typename std::invoke_result_t<t_function, const bool &, t_params...>
      t_ret;

  if constexpr (std::is_void_v<t_ret>) {
    std::thread _th([&]() -> void {
      p_function(_timeout, std::forward<t_params>(p_params)...);
      _cond.notify_one();
    });

    std::unique_lock<std::mutex> _lock{_mutex};
    if (_cond.wait_for(_lock, p_max_time) != std::cv_status::timeout) {
      _th.join();
      return true;
    }
    
    _timeout = true;
    _th.detach();
    return false;
  } else {
    t_ret _ret;

    std::thread _th([&]() -> void {
      _ret = p_function(_timeout, std::forward<t_params>(p_params)...);
      _cond.notify_one();
    });

    std::unique_lock<std::mutex> _lock{_mutex};
    if (_cond.wait_for(_lock, p_max_time) != std::cv_status::timeout) {
      _th.join();
      return {std::move(_ret)};
    }
    _timeout = true;
    
    _th.detach();
    return {};
  }
}

Unlike the code in the answers for the question I referenced, I would not like to throw an exception in the execute wrapper. If p_function does not return, the wrapper will return a bool, true if p_function executed in at most p_max_time, false otherwise. If p_function returns T, the wrapper will return std::optional<T> which will have a value if p_function does not exceed p_max_time, or it will be empty otherwise.

The const bool & parameter required for p_function is used to inform p_function that its execution exceeded p_max_time, so p_function may stop its execution, though execute will not count on it.

Here is an example:

    auto _function = [](const bool &p_is_timeout, int &&p_i) -> void {
      std::this_thread::sleep_for(1s);
      if (p_is_timeout) {
        std::cout << "timeout";
      } else {
        std::cout << "i = " << p_i << '\n';
      }
    };
    int _i{4};

    if (!async::execute(200ms, _function, std::move(_i))) {
      std::cout << "TIMEOUT!!\n";
    }

So, the problem is _th.detach() causes crash when I execute some test functions in a row. I if change it to _th.join(), the crash no longer occurs, but, obviously, the function that calls the wrapper has to wait for p_function to end, which is not desired.

How can I make execute detach _th thread, without causing crash?

CodePudding user response:

Your lambda needs access to the local variables _ret and cond these don't exist after the end of execute so your code has undefined behaviour. Lambdas that capture by reference should only be used when the lambda has the same lifetime as the code that they're defined in.

You'd need to define your state variables in the heap so that they exist after the end of the function. For example you could use a shared_ptr:

template <typename Result>
struct state
{
  std::mutex _mutex;
  std::condition_variable _cond;
  Result _ret;
};
template <>
struct state<void>
{
  std::mutex _mutex;
  std::condition_variable _cond;
};

template <typename t_time, typename t_function, typename... t_params>
inline std::conditional_t<
    // if 't_function' return type is 'void'
    std::is_void_v<std::invoke_result_t<t_function, const bool &, t_params...>>,
    // the 'execute' wrapper will return 'bool', which will be 'true' if the
    // 'p_function' executes in less 'p_max_time', or 'false' otherwise
    bool,
    // else it will result a 'std::optional' with the return type of
    // 't_function', which will contain a value of that type, if the
    // 'p_function' executes in less 'p_max_time', or empty otherwise
    std::optional<std::invoke_result_t<t_function, const bool &, t_params...>>>
execute(t_time p_max_time, t_function &p_function, t_params &&... p_params) {
  typedef typename std::invoke_result_t<t_function, const bool &, t_params...>
      t_ret;
  auto _state = std::make_shared<state<t_ret>>();
  bool _timeout{false};

  if constexpr (std::is_void_v<t_ret>) {
    std::thread _th([&, _state]() -> void {
      p_function(_timeout, std::forward<t_params>(p_params)...);
      _state->_cond.notify_one();
    });

    std::unique_lock<std::mutex> _lock{_state->_mutex};
    if (_state->_cond.wait_for(_lock, p_max_time) != std::cv_status::timeout) {
      _th.join();
      return true;
    }
    
    _timeout = true;
    _th.detach();
    return false;
  } else {
    std::thread _th([&, _state]() -> void {
      _state->_ret = p_function(_timeout, std::forward<t_params>(p_params)...);
      _state->_cond.notify_one();
    });

    std::unique_lock<std::mutex> _lock{_state->_mutex};
    if (_state->_cond.wait_for(_lock, p_max_time) != std::cv_status::timeout) {
      _th.join();
      return {std::move(_state->_ret)};
    }
    _timeout = true;
    
    _th.detach();
    return {};
  }
}

Note for simplicity I've left in the capture of the function arguments and function by reference but you still need to ensure those references remain valid for as long as they're needed (e.g. if the timeout is short the function could exit before the target function executes or if the arguments are references then the invoked function can't use those references after the function exits).

If you have c 20 you might want to look into std::jthread

CodePudding user response:

Based on answers and suggestions, I came up with:

template <typename t_time, typename t_function, typename... t_params>
inline std::conditional_t<
    // if 't_function' return type is 'void'
    std::is_void_v<
        std::invoke_result_t<t_function, std::function<bool()>, t_params...>>,
    // the 'execute' wrapper will return 'bool', which will be 'true' if the
    // 'p_function' executes in less 'p_max_time', or 'false' otherwise
    bool,
    // else it will result a 'std::optional' with the return type of
    // 't_function', which will contain a value of that type, if the
    // 'p_function' executes in less 'p_max_time', or empty otherwise
    std::optional<
        std::invoke_result_t<t_function, std::function<bool()>, t_params...>>>
execute(t_time p_max_time, t_function &p_function, t_params &&... p_params) {
  std::mutex _mutex;
  std::condition_variable _cond;
  auto _timeout = std::make_shared<bool>(false);

  auto _is_timeout = [_timeout]() { return *_timeout; };

  typedef typename std::invoke_result_t<t_function, std::function<bool()>,
                                        t_params...>
      t_ret;

  if constexpr (std::is_void_v<t_ret>) {
    std::thread _th([&]() -> void {
      p_function(_is_timeout, std::forward<t_params>(p_params)...);
      _cond.notify_one();
    });

    std::unique_lock<std::mutex> _lock{_mutex};
    if (_cond.wait_for(_lock, p_max_time) != std::cv_status::timeout) {
      _th.join();
      return true;
    }
    *_timeout = true;
    _th.join();
    return false;
  } else {
    t_ret _ret;

    std::thread _th([&]() -> void {
      _ret = p_function(_is_timeout, std::forward<t_params>(p_params)...);
      _cond.notify_one();
    });

    std::unique_lock<std::mutex> _lock{_mutex};
    if (_cond.wait_for(_lock, p_max_time) != std::cv_status::timeout) {
      _th.join();
      return {std::move(_ret)};
    }
    *_timeout = true;
    _th.join();
    return {};
  }
}

And the example becomes:

    auto _function = [](std::function<bool()> p_timeout, int &&p_i) -> void {
      std::this_thread::sleep_for(1s);
      if (p_timeout()) {
        std::cout << "timeout in work function\n";
      } else {
        std::cout << "i = " << p_i  << '\n';
      }
    };
    int _i{4};

    if (!execute(200ms, _function, std::move(_i))) {
       std::cout << "OK - timeout\n";
    }
    else {
       std::cout << "NOT OK - no timeout\n";
    }

I believe passing a std::function<bool()> to the work function (p_function) creates a good abstraction on how execute will control the timeout, and an easy way for p_function to check for it.

I also removed the std::thread::detach() calls.

  • Related