Return values for active objects
Asked Answered
C

1

9

Back in 2010, Herb Sutter advocated the use of active objects instead of naked threads in an article on Dr. Dobb's.

Here is a C++11 version:

class Active {
public:
    typedef std::function<void()> Message;

    Active(const Active&) = delete;
    void operator=(const Active&) = delete;

    Active() : done(false) {
        thd = std::unique_ptr<std::thread>(new std::thread( [=]{ this->run(); } ) );
    }

    ~Active() {
        send( [&]{ done = true; } );
        thd->join();
    }

    void send(Message m) { mq.push_back(m); }

private:
    bool done;
    message_queue<Message> mq; // a thread-safe concurrent queue
    std::unique_ptr<std::thread> thd;

    void run() {
        while (!done) {
            Message msg = mq.pop_front();
            msg(); // execute message
        } // note: last message sets done to true
    }
};

The class can be used like this:

class Backgrounder {
public:
    void save(std::string filename) { a.send( [=] {
        // ...
    } ); }

    void print(Data& data) { a.send( [=, &data] {
        // ...
    } ); }

private:
    PrivateData somePrivateStateAcrossCalls;
    Active a;
};

I would like to support member functions with non-void return types. But I cannot come up with a nice design how to implement this, i.e. without using a container that can hold objects of heterogeneous types (like boost::any).

Any ideas are welcome, especially answers that make use of C++11 features like std::future and std::promise.

Chopper answered 15/6, 2015 at 20:5 Comment(4)
Are you asking for the (unloved) std::async ?Alpenglow
If I read the reference right, std::async launches new threads or uses a thread pool. I would like to queue tasks on a single thread for sequential execution.Chopper
In Herb Sutter's example, what's the rationale behind Active holding a std::unique_ptr<std::thread> instead of just a plain std::thread?Attired
@Attired Active Objects actually finish processing the tasks in its queue inside the destructor. So, the way you get it to finish its work is by allowing it to go out of scope. The unique_ptr needs to be there so the thread is the last thing to be deleted, even after the Active Object. Once the ptr goes out of scope, it will delete the thread.Coarsen
R
13

This will take some work.

First, write task<Sig>. task<Sig> is a std::function that only expects its argument to be movable, not copyable.

Your internal type Messages are going to be task<void()>. So you can be lazy and have your task only support nullary functions if you like.

Second, send creates a std::packaged_task<R> package(f);. It then gets the future out of the task, and then moves the package into your queue of messages. (This is why you need a move-only std::function, because packaged_task can only be moved).

You then return the future from the packaged_task.

template<class F, class R=std::result_of_t<F const&()>>
std::future<R> send(F&& f) {
  packaged_task<R> package(std::forward<F>(f));
  auto ret = package.get_future();
  mq.push_back( std::move(package) );
  return ret;
}

clients can grab ahold of the std::future and use it to (later) get the result of the call back.

Amusingly, you can write a really simple move-only nullary task as follows:

template<class R>
struct task {
  std::packaged_task<R> state;
  template<class F>
  task( F&& f ):state(std::forward<F>(f)) {}
  R operator()() const {
    auto fut = state.get_future();
    state();
    return f.get();
  }
};

but that is ridiculously inefficient (packaged task has synchronization stuff in it), and probably needs some cleanup. I find it amusing because it uses a packaged_task for the move-only std::function part.

Personally, I've run into enough reasons to want move-only tasks (among this problem) to feel that a move-only std::function is worth writing. What follows is one such implementation. It isn't heavily optimized (probably about as fast as most std::function however), and not debugged, but the design is sound:

template<class Sig>
struct task;
namespace details_task {
  template<class Sig>
  struct ipimpl;
  template<class R, class...Args>
  struct ipimpl<R(Args...)> {
    virtual ~ipimpl() {}
    virtual R invoke(Args&&...args) const = 0;
  };
  template<class Sig, class F>
  struct pimpl;
  template<class R, class...Args, class F>
  struct pimpl<R(Args...), F>:ipimpl<R(Args...)> {
    F f;
    R invoke(Args&&...args) const final override {
      return f(std::forward<Args>(args)...);
    };
  };
  // void case, we don't care about what f returns:
  template<class...Args, class F>
  struct pimpl<void(Args...), F>:ipimpl<void(Args...)> {
    F f;
    template<class Fin>
    pimpl(Fin&&fin):f(std::forward<Fin>(fin)){}
    void invoke(Args&&...args) const final override {
      f(std::forward<Args>(args)...);
    };
  };
}
template<class R, class...Args>
struct task<R(Args...)> {
  std::unique_ptr< details_task::ipimpl<R(Args...)> > pimpl;
  task(task&&)=default;
  task&operator=(task&&)=default;
  task()=default;
  explicit operator bool() const { return static_cast<bool>(pimpl); }

  R operator()(Args...args) const {
    return pimpl->invoke(std::forward<Args>(args)...);
  }
  // if we can be called with the signature, use this:
  template<class F, class=std::enable_if_t<
    std::is_convertible<std::result_of_t<F const&(Args...)>,R>{}
  >>
  task(F&& f):task(std::forward<F>(f), std::is_convertible<F&,bool>{}) {}

  // the case where we are a void return type, we don't
  // care what the return type of F is, just that we can call it:
  template<class F, class R2=R, class=std::result_of_t<F const&(Args...)>,
    class=std::enable_if_t<std::is_same<R2, void>{}>
  >
  task(F&& f):task(std::forward<F>(f), std::is_convertible<F&,bool>{}) {}

  // this helps with overload resolution in some cases:
  task( R(*pf)(Args...) ):task(pf, std::true_type{}) {}
  // = nullptr support:
  task( std::nullptr_t ):task() {}

private:
  // build a pimpl from F.  All ctors get here, or to task() eventually:
  template<class F>
  task( F&& f, std::false_type /* needs a test?  No! */ ):
    pimpl( new details_task::pimpl<R(Args...), std::decay_t<F>>{ std::forward<F>(f) } )
  {}
  // cast incoming to bool, if it works, construct, otherwise
  // we should be empty:
  // move-constructs, because we need to run-time dispatch between two ctors.
  // if we pass the test, dispatch to task(?, false_type) (no test needed)
  // if we fail the test, dispatch to task() (empty task).
  template<class F>
  task( F&& f, std::true_type /* needs a test?  Yes! */ ):
    task( f?task( std::forward<F>(f), std::false_type{} ):task() )
  {}
};

live example.

is a first sketch at a library-class move-only task object. It also uses some C++14 stuff (the std::blah_t aliases) -- replace std::enable_if_t<???> with typename std::enable_if<???>::type if you are a C++11-only compiler.

Note that the void return type trick contains some marginally questionable template overload tricks. (It is arguable if it is legal under the wording of the standard, but every C++11 compiler will accept it, and it is likely to become legal if it is not).

Roswell answered 15/6, 2015 at 21:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.