1

I'm wondering how to develop an asynchronous API using promises and futures. The application is using a single data stream that is used for both unsolicited periodic data and requesty/reply communication.

For the requesty/reply blocking until the reply is received is not an option and I don't want lo litter the code using callbacks, so I'd like to write some kind of a SendMessage that accepts the id of the expected reply and exits only upon reception. It's up to the caller to read the reply.

A candidate API could be:

std::future<void> sendMessage(Message msg, id expected)
{
   // Write the message
   auto promise = make_shared<std::promise<void>>();
   // Memorize the promise somewhere accessible to the receiving thread
   return promise->get_future();
 }

The worker thread upon reception of a message should be able to query a data-structure to know if there is someone waiting for it and "release" the future.

Given that promises are not re-usable what I'm trying to understand is what kind of data-structure should I use to manage "in flight" promises.

PeppeDx
  • 133
  • 1
  • 10
  • I am not sure whether I fully understand your question. The worker holds a weak_ptr of the promise, while the other side holds a shared_ptr of the same promise but can only have access to the future which is bind with it. The worker can then check the validation of the weak_ptr to tell whether anyone is still expecting the result. – felix Feb 10 '17 at 11:24
  • @felix I think you've understood. So my API should return the whole promise (shared_ptr) and not the future ? – PeppeDx Feb 10 '17 at 11:29
  • Yes, if you need notify the worker when no one is expecting the result on destruction of the shared_ptr. Note: There will be some race condition in my solution. At least, when the worker decide to push value into the promise, something should block others from release the promise at this moment. You may need some kind of thread safe version of shared_ptr. – felix Feb 10 '17 at 11:42
  • @felix for me this is an answer... – PeppeDx Feb 10 '17 at 13:58

1 Answers1

2

This answer has been rewritten.

Setting the state of a shared flag can enable the worker to know whether the other side, say boss, is still expecting the result.

The shared flag along with the promise and the future can be enclosed into a class (template), say Request. The boss set the flag by destructing his copy of the request. And the worker query whether the boss is still expecting the request being done by calling certain member function on his own copy of the request.

Simultaneous reading/writing on the flag should be probably synchronized.

The boss may not access the promise and the worker may not access the future.

There should be at most two copies of the request, becaue the flag will be set on the destruction of the request object. For achieving this, we can delcare corresponding member functions as delete or private, and provide two copies of the request on construction.

Here follows a simple implementation of request:

#include <atomic>
#include <future>
#include <memory>

template <class T>
class Request {
 public:
  struct Detail {
   std::atomic<bool> is_canceled_{false};
   std::promise<T> promise_;
   std::future<T> future_ = promise_.get_future();
  };

  static auto NewRequest() {
    std::unique_ptr<Request> copy1{new Request()};
    std::unique_ptr<Request> copy2{new Request(*copy1)};

    return std::make_pair(std::move(copy1), std::move(copy2));
  }

  Request(Request &&) = delete;

  ~Request() {
    detail_->is_canceled_.store(true);
  }

  Request &operator=(const Request &) = delete;
  Request &operator=(Request &&) = delete;

  // simple api
  std::promise<T> &Promise(const WorkerType &) {
    return detail_->promise_;
  }
  std::future<T> &Future(const BossType &) {
    return detail_->future_;
  }

  // return value:
  // true if available, false otherwise
  bool CheckAvailable() {
    return detail_->is_canceled_.load() == false;
  }

 private:
  Request() : detail_(new Detail{}) {}
  Request(const Request &) = default;

  std::shared_ptr<Detail> detail_;
};

template <class T>
auto SendMessage() {
  auto result = Request<T>::NewRequest();
  // TODO : send result.second(the another copy) to the worker
  return std::move(result.first);
}

New request is contructed by factroy function NewRequest, the return value is a std::pair which contains two std::unique_ptr, each hold a copy of the newly created request.

The worker can now use the member function CheckAvailable() to check whether the request is canceled.

And the shared state is managed proprely(I believe) by the std::shared_ptr.

Note on std::promise<T> &Promise(const WorkerType &): The const reference parameter(which should be replaced with a propre type according to your implementation) is for preventing the boss from calling this function by accident while the worker should be able to easily provide a propre argument for calling this function. The same for std::future<T> &Future(const BossType &).

felix
  • 2,213
  • 7
  • 16
  • 2
    Thread-safe in what way? How does it differ from `shared_ptr`? Where is it defined? (It's not in any of the headers your example includes). – Jonathan Wakely Feb 13 '17 at 13:51
  • @JonathanWakely Thanks for helping me with improving my answer, shouldn't think that this won't be necessary. – felix Feb 13 '17 at 14:31
  • The reason I ask is that "thread-safe" is not a well-defined term. Safe in what way? And apparently you don't realise that `shared_ptr` always has a "thread-safe" copy constructor, because you say _"a thread-safe copy constructor/assignement operator won't be necessary"_. Why does any type ever need a thread-safe destructor? It's undefined behaviour to access an object during its destructor, so a "thread-safe destructor" is impossible in a correct C++ program. So it seems my questions are justified, and even your improved answer is unclear and confusing. – Jonathan Wakely Feb 13 '17 at 14:55
  • That's not true. `std::shared_ptr` has a copy constructor that cannot cause data races. This can be achieved either by locking a mutex while increasing the reference count, or by using atomic operations to increase the reference count. – Jonathan Wakely Feb 13 '17 at 15:10
  • Sorry, I have mistaken your question. By saying "thread-safe destructor", I mean, a destructor which will not cause any race condition. – felix Feb 13 '17 at 15:13
  • There are definitly lots of things which i don't know, and lots of things which I thought I know for sure but in fact I don't. I am not going to learn them all now or here. But, please, feel free to improve my answer or edit your own. – felix Feb 13 '17 at 15:16
  • @JonathanWakely I apologize for my behavior, I was over reacted and rude. Your point about my former answer is right, I should have listened to you humbly. I have just tried to improve my answer, I will keep tracking this question until there is a good answer. – felix Feb 14 '17 at 13:42
  • Looks much better now, without relying on some imaginary type that isn't clearly described - thanks! – Jonathan Wakely Feb 14 '17 at 16:21