eidheim / Simple-WebSocket-Server

A very simple, fast, multithreaded, platform independent WebSocket (WS) and WebSocket Secure (WSS) server and client library implemented using C++11, Boost.Asio and OpenSSL. Created to be an easy way to make WebSocket endpoints in C++.
MIT License
800 stars 279 forks source link

how to get synchronous send ? how to wait() on async call ? #72

Open timotheecour opened 7 years ago

timotheecour commented 7 years ago

@eidheim

How can I call wait after a send (and avoid the "callback hell" pattern (https://colintoh.com/blog/staying-sane-with-asynchronous-programming-promises-and-generators) that you suggested in https://github.com/eidheim/Simple-WebSocket-Server/blob/master/ws_examples.cpp#L79 in response to https://github.com/eidheim/Simple-WebSocket-Server/issues/24 ?

EDIT: this seems very relevant: https://stackoverflow.com/questions/20709725/how-to-wait-for-an-asio-handler

void bar(int value){
    typedef boost::promise<void> promise_type;
    promise_type promise;

    // Pass the handler to async operation that will set the promise.
    async_set_bar(value, boost::bind(&promise_type::set_value, &promise));

    // Synchronously wait for the future to finish.
    promise.get_future().wait();
  }
eidheim commented 7 years ago

Thank you for starting this discussion, it is very interesting.

First off, get_future().wait() is blocking and should not be run inside an io_service task.

Secondly, the PPL Tasks are interesting, and if I remember correctly they are proposed for the C++ standard, but will not be accepted before earliest C++20. The main problem here is that using PPL Tasks will greatly affect how you write your program. You can't for instance call await inside a regular function (that is for instance returning void) (edit: this is not entirely correct, there are ways to synchronise async calls through blocking). This added complexity somewhat counteracts the advantages of working with an event-loop.

With respect to waiting (in a nonblocking manner) for async functions, the boost way is to use boost::asio::spawn. See for instance: http://www.boost.org/doc/libs/1_64_0/doc/html/boost_asio/example/cpp11/spawn/echo_server.cpp. The problem here though is that you run functions, that could potentially run in parallell, sequentially. This is also an issue with PPL's await pattern. Additionally, the stack handing is more complex.

Finally, we have our current solution, that has none of the above drawbacks, but with the drawback of potentially leading to many nested callbacks. There is no perfect solution in my opinion, but will be following the c++ standard committee's work on asio and related additions.

eidheim commented 7 years ago

JavaScript's Promise is actually my favourite way of chaining async calls, but to my knowledge this is currently not possible to do with c++'s (boost::)asio. One problem I guess would be to keep the promise/future objects alive when leaving scope.

timotheecour commented 7 years ago

@eidheim

How about this?


http://www.boost.org/doc/libs/1_55_0/doc/html/thread/synchronization.html#thread.synchronization.futures.reference.unique_future.then

include <boost/thread/future.hpp>

using namespace boost; int main() { future f1 = async([]() { return 123; }); future f2 = f1.then([](future f) { return f.get().to_string(); // here .get() won't block }); }

eidheim commented 7 years ago

In http://www.boost.org/doc/libs/1_64_0/doc/html/thread/synchronization.html#thread.synchronization.futures.reference.unique_future.then you have the line (under Notes): "The returned futures behave as the ones returned from boost::async, the destructor of the future object returned from then will block. This could be subject to change in future versions.". One would have to currently keep the future object alive then, or else it will block.

I'll look into this further in the following days.

timotheecour commented 7 years ago

relavant to what you just said: https://stackoverflow.com/a/12527892/1426932 create the promise/future pair right in run(), and pass the promise to the thread

void callback(std::promise<void> p)
    {
        _job();
        p.set_value();
    }
eidheim commented 7 years ago

In your above link, synchronous_job::run is blocking. Our case is a bit different than the one described in the link, since both the promise/future and the callback should run in the same thread, for instance in the event loop (which is the default threading strategy in Simple-Web(Socket)-Server). Additionally, the promise/future should be kept alive without returning the future (as in JavaScript where the promises are kept alive through garbage collection I guess). I think PPL Tasks solves this by always returning the tasks until eventually a wait() (or something similar) is called on the task chain.

eidheim commented 7 years ago

I was a bit unclear above, what I mean by running in the same thread, is that future::wait() should never be called, and future::get should only be called within the future::then callback, that is after promise::set is called. The future::then callback should also be run within a posted io_service task, and promise::set should be called within an io_service task as well (but a different posted task).

edit: changed future::then to: the future::then callback