#Need some explanations for this multithreaded example...

5 messages · Page 1 of 1 (latest)

hybrid dawn
#

Assume we have the sequential code:

void process_login(std::string const& username,std::string const& password) {
  try {
      user_id const id=backend.authenticate_user(username,password);
      user_data const info_to_display=backend.request_current_info(id);
      update_display(info_to_display);
  } catch(std::exception& e) {
      display_error(e);
  }
}

Which we want to make concurrent, since we don't want to block the UI Thread. So we first do it with a single std::async [2]

std::future<void> process_login(std::string const& username,std::string const& password) {
  return std::async(std::launch::async,[=]() {
    try {
        user_id const id=backend.authenticate_user(username,password);
        user_data const info_to_display=
        backend.request_current_info(id);
        update_display(info_to_display);
    } catch(std::exception& e){
        display_error(e);
 }});
}

However according to the author: "using one std::async would still block the new thread, consuming resources while waiting for the tasks to complete."

atomic zealotBOT
#

When your question is answered use !solved to mark the question as resolved.

Remember to ask specific questions, provide necessary details, and reduce your question to its simplest form. For tips on how to ask a good question run !howto ask.

hybrid dawn
#

Therefore, they decide to use continuations with std::experimental::future::then [3]

std::experimental::future<void> process_login(std::string const& username,std::string const& password) {
   return spawn_async([=](){
     return backend.authenticate_user(username,password);
   }).then([](std::experimental::future<user_id> id){
     return backend.request_current_info(id.get());
   }).then([](std::experimental::future<user_data> info_to_display){
     try{
        update_display(info_to_display.get());
     } catch(std::exception& e){
       display_error(e);
     }
 });
}

However, according to the author: "If the function calls to the backend block internally because they’re waiting for messages to cross the network or for a database operation to complete, then you’re
not done yet. You may have split the task into its individual parts, but they’re still
blocking calls, so you still get blocked threads
"
(Note: spawn_async is basically std::async for continuations, as follows:)

template<typename Func>
std::experimental::future<decltype(std::declval<Func>()())> spawn_async(Func&& func){
   std::experimental::promise<
   decltype(std::declval<Func>()())> p;
   auto res=p.get_future();
   std::thread t(
     [p=std::move(p),f=std::decay_t<Func>  (func)]() mutable{
     try {
        p.set_value_at_thread_exit(f());
     } catch(...) {         p.set_exception_at_thread_exit(std::current_exception());
     }
  });
  t.detach();
  return res;
}
#

^Still slow because of the same reasons as [2]. The author thus says: "You may have split the task into its individual parts, but they’re still
blocking calls, so you still get blocked threads. What you need is for the backend calls
to return futures that become ready when the data is ready, without blocking any
threads. In this case, backend.async_authenticate_user(username,password) will
now return a std::experimental::future<user_id> rather than a plain user_id
" [4]
So this is what the author ends up with:

std::experimental::future<void> process_login(std::string const& username,std::string const& password) {
   return backend.async_authenticate_user(username,password).then(
     [](std::experimental::future<user_id> id) {
       return backend.async_request_current_info(id.get());
   }).then([](std::experimental::future<user_data> info_to_display){
       try{
          update_display(info_to_display.get());
       } catch(std::exception& e){
          display_error(e);
   }});
}

...Apparently, the latest snippet is "the best" because "no threads are blocked"

So my questions:

  • I'm not really sure what difference there is between [2] and [3]. Sure [2] single tasks (such as backend.request_current_info) might block, but in [3] the same task might block as well, and then() will only proceed when the task before ended...
  • How exactly isn't the same thread blocked in [4], given that futures' then() proceeds only when the function before is done? How is this last snippet different from, say, the execution of [2] and [3]?
#

(Pretty long question yeah, if needed I can give the page on C++ Concurreny In Action to show the full thing if it's unclear....)