我目前正在阅读
C++ Concurrency in Action 2nd edition
,第 111 页(第 4 章)。这是一个有点长的问题。
因此,作者展示了如何编写一个处理用户登录的函数的良好并发版本。 这是第一个连续片段:
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);
}
}
[1]第一个尝试是使用
std::async
,这样整个过程都在一个新的线程中完成,如:
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);
}
});
}
但是,根据作者的说法,[1] 不太好,因为“使用普通的 std::async,您可以将其全部转入后台线程,如下一个清单,但这仍然会阻塞该线程,消耗资源在等待任务完成时”
[2] 因此,作者展示了一种新的处理方法,通过使用延续和链接任务:
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);
}
});
}
其中
spawn_async
是:
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;
}
但是,根据作者的说法,这仍然很糟糕,因为“如果函数在内部调用后端块,因为它们正在等待 消息通过网络或完成数据库操作,那么您 还没做完。您可能已将任务拆分为各个部分,但它们仍然是 阻塞调用,因此您仍然会遇到阻塞线程。您需要的是后端调用 返回当数据准备好时准备好的 future,而不阻塞任何 线程”
[3] 所以最后一段代码是:
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);
}
});
}
其中
backend
函数返回 experimental::future
。
所以这就是所有的疑问。
backend.request_current_info
)可能会阻塞,但在 [2] 中,同一个任务也可能会阻塞,然后 then() 仅当之前的任务结束时才会继续。future
的 .then()
函数仅在前一个任务完成后才继续执行,那么线程在最后一个片段 [3] 中到底如何不被阻塞?我也无法理解第一个问题,尤其是作者后来承认:
“如果函数在内部调用后端块,因为它们正在等待消息穿过网络或等待数据库操作完成,那么您还没有完成。”
对于第二个问题,也许我已经明白了。
我认为作者的意思是
authenticate_user
就像一个 while
循环,不断检查网络响应是否已到来,而 async_authenticate_user
只是将 promise
对象传递给某个网络框架,该框架会在每一帧上打勾当等待的响应到达时,致电 promise.set_value
。所以后面的实现不会再阻塞线程了。