Search code examples
c++c++11currencyfutureproducer-consumer

Why there is no std::future::try_wait()?


Given that there are std::future::wait_for/until(), I don't see why there is no std::future::try_wait(). I'm currently writing a producer-consumer example, and I want to use std::future as a convenient way to signal the consumer threads to return. My consumer code is like

void consume(std::future<void>& stop) {
  while (!stop.try_wait()) { // alas, no such method
    // try consuming an item in queue
  }
}

I'm thinking to simulate try_wait() with a zero-duration wait_for() which is really ugly. As a side question: any other convenient ways to signal the consumer threads to return?


Solution

  • std::experimental::future has a .is_ready() and .then( F ) methods added to it.

    is_ready is probably your try_wait (without a timeout).

    wait_for, as noted, gives you the functionality of try_wait in practice.


    std::future is not designed as a signaling mechanism even if it can be used as one. If you want a signaling mechansim, create one using a condition variable, mutex, and state that stores the state of the signals (possibly combining them).

    struct state {
      bool stop = false;
      unsigned some_value = 7;
      friend auto as_tie( state const& s ) {
        return std::tie(s.stop, s.some_value);
      }
      friend bool operator==( state const& lhs, state const& rhs ) {
        return as_tie(lhs)==as_tie(rhs);
      }
    };
    
    template<class State, class Cmp=std::equal<State>>
    struct condition_state {
      // gets a copy of the current state:
      State get_state() const {
        auto l = lock();
        return state;
      }
      // Returns a state that is different than in:
      State next_state(State const& in) const {
        auto l = lock();
        cv.wait( l, [&]{ return !Cmp{}(in, state); } );
        return state;
      }
      // runs f on the state if it changes from old.
      // does this atomically in a mutex, so be careful.
      template<class F>
      auto consume_state( F&& f, State old ) const {
        auto l = lock();
        cv.wait( l, [&]{ return !Cmp{}(old, state); } );
        return std::forward<F>(f)( state );
      }
      // runs f on the state if it changes:
      template<class F>
      auto consume_state( F&& f ) const {
        return consume_state( std::forward<F>(f), state );
      }
      // calls f on the state, then notifies everyone to check if
      // it has changed:
      template<class F>
      void change_state( F&& f ) {
        {
          auto l = lock();
          std::forward<F>(f)( state );
        }
        cv.notify_all();
      }
      // Sets the value of state to in
      void set_state( State in ) {
        change_state( [&](State& state) {
          state = std::move(in);
        } );
      }
    private:
      auto lock() const { return std::unique_lock<std::mutex>(m); }
      mutable std::mutex m;
      std::condition_variable cv;
      State state;
    };
    

    For an example, suppose our State was a vector of ready tasks and a bool saying "abort":

    struct tasks_todo {
      std::deque< std::function<void()> > todo;
      bool abort = false;
      friend bool operator==()( tasks_todo const& lhs, tasks_todo const& rhs ) {
        if (lhs.abort != rhs.abort) return false;
        if (lhs.todo.size() != rhs.todo.size()) return false;
        return true;
      }
    };
    

    then we can write our queue as follows:

    struct task_queue {
      void add_task( std::function<void()> task ) {
        tasks.change_state( [&](auto& tasks) { tasks.todo.push_back(std::move(task)); } );
      }
      void shutdown() {
        tasks.change_state( [&](auto& tasks) { tasks.abort = true; } );
      }
      std::function<void()> pop_task() {
        return tasks.consume_state(
          [&](auto& tasks)->std::function<void()> {
            if (tasks.abort) return {};
            if (tasks.todo.empty()) return {}; // should be impossible
            auto r = tasks.front();
            tasks.pop_front();
            return r;
          },
          {} // non-aborted empty queue
        );
      }
    private:
      condition_state<task_todo> tasks;
    };
    

    or somesuch.