同步VS异步
Boost.Asio的作者做了一个很惊艳的工作:它可以让你在同步和异步中自由选择,从而更好地适应你的应用。
在之前的章节中,我们已经学习了各种类型应用的框架,比如同步客户端,同步服务端,异步客户端,异步服务端。它们中的每一个都可以作为你应用的基础。如果要更加深入地学习各种类型应用的细节,请继续。
混合同步异步编程
Boost.Asio库允许你进行同步和异步的混合编程。我个人认为这是一个坏主意,但是Boost.Asio(就像C++一样)在你需要的时候允许你深入底层。
通常来说,当你写一个异步应用时,你会很容易掉入这个陷阱。比如在响应一个异步write操作时,你做了一个同步read操作:
io_service service;
ip::tcp::socket sock(service);
ip::tcp::endpoint ep(ip::address::from_string("127.0.0.1"), 8001);
void on_write(boost::system::error_code err, size_t bytes) {
char read_buff[512];
read(sock, buffer(read_buff));
}
async_write(sock, buffer("echo"), on_write);
毫无疑问,同步的 read 操作会阻塞当前线程,从而导致这个线程上其他正在等待的异步操作全部挂起。这是一段糟糕的代码,因为它会导致整个应用变得无响应甚至完全阻塞(所有异步操作都必须避免阻塞,而执行一个同步操作恰恰违反了这个原则)。
当你写一个同步应用时,你不大可能执行异步的read或者write操作,因为同步地思考已经意味着用一种线性的方式思考(执行A,然后执行B,再执行C,等等)。
我唯一能想到的同步和异步同时工作的场景就是同步操作和异步操作是完全隔离的,比如,同步和异步从一个数据库进行读写。
从客户端传递信息到服务端VS从服务端传递信息到客户端
成功的客户端/服务端应用一个很重要的部分就是来回传递消息(服务端到客户端和客户端到服务端)。你需要指定用什么来标记一个消息。换句话说,当读取一个输入的消息时,你怎么判断它被完整读取了?
标记消息结尾的方式完全取决于你(标记消息的开始很简单,因为它就是前一个消息之后传递过来的第一个字节),但是要保证消息是简单且连续的。
你可以:
- 使用某个特殊字符来标记消息的结尾,比如
'\n' - 在消息头部指定消息的长度
在我们之前的所有示例中,采用的都是「用 ‘\n’ 标记消息结尾」的方式。所以,每次读取一条消息都是如下这样:
char buff_[512];
// 同步读取
read(sock_, buffer(buff_), boost::bind(&read_complete, this, _1, _2));
// 异步读取
async_read(sock_, buffer(buff_), MEM_FN2(read_complete,_1,_2), MEM_FN2(on_read,_1,_2));
size_t read_complete(const boost::system::error_code &err, size_t bytes) {
if (err) return 0;
already_read_ = bytes;
bool found = std::find(buff_, buff_ + bytes, '\n') < buff_ + bytes;
// 一个一个读,直到读到回车,无缓存
return found ? 0 : 1;
}
我把在消息头部指定消息长度这种方式作为一个练习留给读者;这非常简单。
客户端应用中的同步I/O
同步客户端一般都能归类到如下两种情况中的一种:
- 拉取式:客户端向服务端发送请求,然后读取服务端返回的结果。
- 推送式:服务端主动向客户端发送通知,客户端读取通知并做出回复。
两种情况都使用”发送请求-读取结果”的策略。换句话说,一个部分发送一个请求到另外一个部分然后另外一个部分返回结果。这是实现客户端/服务端应用非常简单的一种方式,同时这也是我非常推荐的一种方式。
你可以创建一个大杂烩(Mambo Jambo)式的客户端/服务端应用,随心所欲地在两端写任何逻辑,但这会导致一场灾难——当客户端或者服务端阻塞的时候会发生什么?
上面两种情况看上去相似,但它们非常不同:
- 你大部分时间都在做请求式客户端/服务端应用,这也是比较简单,同时也是比较常见的。
- 你可以把拉取请求(客户端到服务端)和推送请求(服务端到客户端)结合起来,但是,这是非常复杂的,所以你最好避免这种情况。
把这两种方式结合的问题在于:如果你使用”发送请求-读取结果”策略,服务端只能在收到客户端的请求之后才能回复,无法主动向客户端推送消息。
在一个请求式客户端/服务端应用中,模拟上面的推送场景也非常简单:你可以实现一个 ping 操作来模拟推送式请求,假设客户端每 5 秒 ping 一次服务端。如果没有事情需要通知,服务端就返回一个类似 ping ok 的结果;如果有事情需要通知,服务端返回 ping [event_name]。然后客户端就可以发起一个新的请求去处理这个事件。
复习一下,第一种情况就是《Boost.Asio:客户端和服务端》中的同步客户端应用,它的主循环如下:
void loop() {
// 对于我们登录操作的结果
write("login " + username_ + "\n");
read_answer();
while (started_) {
write_request();
read_answer();
...
}
}
我们对其进行修改以适应第二种情况:
void loop() {
while (started_) {
read_notification();
write_answer();
}
}
void read_notification() {
already_read_ = 0;
read(sock_, buffer(buff_), boost::bind(&talk_to_svr::read_complete, this, _1, _2));
process_notification();
}
void process_notification() {
// ... 看通知是什么,然后准备回复
}
服务端应用中的同步I/O
类似客户端,服务端也被分为两种情况用来匹配之前章节中的情况1和情况2。同样,两种情况都采用”发送请求-读取结果”的策略。
第一种情况是我们在之前章节实现过的同步服务端。当你采用同步方式时,读取一条完整的请求并不那么简单,因为你需要避免阻塞(通常的做法是有多少数据就读多少):
void read_request() {
if (sock_.available())
already_read_ += sock_.read_some(buffer(buff_ + already_read_, max_msg - already_read_));
}
只要一个消息被完整读到,就对它进行处理然后回复给客户端:
void process_request() {
bool found_enter = std::find(buff_, buff_ + already_read_, '\n') < buff_ + already_read_;
if (!found_enter)
return; // 消息不完整
size_t pos = std::find(buff_, buff_ + already_read_, '\n') - buff_;
std::string msg(buff_, pos);
...
if (msg.find("login ") == 0) on_login(msg);
else if (msg.find("ping") == 0) on_ping();
else ...
}
如果我们想让服务端变成一个推送服务端,我们通过如下的方式修改:
typedef std::vector<client_ptr> array;
array clients;
array notify;
std::string notify_msg;
void on_new_client() {
// 新客户端连接时,我们通知所有客户端这个事件
notify = clients;
std::ostringstream msg;
msg << "client count " << clients.size();
notify_msg = msg.str();
notify_clients();
}
void notify_clients() {
for (array::const_iterator b = notify.begin(), e = notify.end(); b != e; ++b) {
(*b)->sock_.write_some(buffer(notify_msg));
}
}
on_new_client() 就是这样一个事件:发生时我们需要通知所有的客户端。notify_clients() 则是通知所有对该事件感兴趣的客户端的方法。它只发送消息,不等待每个客户端的返回结果,因为那样会阻塞。当客户端返回结果时,它会告诉我们为什么回复(然后我们就可以正确地处理它)。
同步服务端中的线程
这是一个非常重要的关注点:我们要开辟多少线程去处理服务端请求?对一个同步服务端来说,我们至少需要一个处理新连接的线程:
void accept_thread() {
ip::tcp::acceptor acceptor(service, ip::tcp::endpoint(ip::tcp::v4(),8001));
while (true) {
client_ptr new_( new talk_to_client);
acceptor.accept(new_->sock());
boost::recursive_mutex::scoped_lock lk(cs);
clients.push_back(new_);
}
}
对于已经存在的客户端,还有其他几种处理方式,其中最难的是第三种——整个 talk_to_client 类都得是线程安全的。接着,你还需要一个机制来决定由哪个线程处理哪个客户端。针对这个问题有两种做法;下面的代码片段和之前的 answer_to_client() 方法有点类似,它向我们展示了第二种做法的实现方式:
struct talk_to_client : boost::enable_shared_from_this<talk_to_client>
{
...
void answer_to_client() {
try {
read_request();
process_request();
} catch ( boost::system::system_error&) { stop(); }
}
};
我们需要对它进行修改使它变成下面代码片段的样子:
struct talk_to_client : boost::enable_shared_from_this<talk_to_client>
{
boost::recursive_mutex cs;
boost::recursive_mutex cs_ask;
bool in_process;
void answer_to_client() {
{
boost::recursive_mutex::scoped_lock lk(cs_ask);
if (in_process)
return;
in_process = true;
}
{
boost::recursive_mutex::scoped_lock lk(cs);
try {
read_request();
process_request();
} catch (boost::system::system_error&) {
stop();
}
}
{
boost::recursive_mutex::scoped_lock lk(cs_ask);
in_process = false;
}
}
};
当我们在处理某个客户端的请求时,它的 in_process 变量被置为 true,其他线程就会忽略这个客户端。额外的好处是 handle_clients_thread() 方法不需要做任何修改;你可以随意创建任意数量的 handle_clients_thread() 线程。
客户端应用中的异步I/O
主流程和同步客户端应用有点类似,不同的是,Boost.Asio 始终位于 async_read 和 async_write 请求之间。
第一种情况是《Boost.Asio:客户端和服务端》中实现过的。你应该还记得,在每个异步操作结束的时候,我都会启动另外一个异步操作,这样 service.run() 方法才不会结束。
为了适应第二种情况,你需要使用下面的代码片段:
void on_connect() {
do_read();
}
void do_read() {
async_read(sock_, buffer(read_buffer_), MEM_FN2(read_complete,_1,_2), MEM_FN2(on_read,_1,_2));
}
void on_read(const error_code & err, size_t bytes) {
if (err) stop();
if (!started()) return;
std::string msg(read_buffer_, bytes);
if (msg.find("clients") == 0) on_clients(msg);
else ...
}
void on_clients(const std::string & msg) {
std::string clients = msg.substr(8);
std::cout << username_ << ", new client list:" << clients ;
do_write("clients ok\n");
}
注意只要我们成功连接上,就开始从服务端读取。每个 on_[event] 方法最后都会向服务端写回一个应答来收尾。
使用异步的美好在于你可以使用Boost.Asio进行管理,从而把I/O网络操作和其他异步操作结合起来。尽管它的流程不像同步的流程那么清晰,你仍然可以用同步的方式来想象它。
假设,你从一个web服务器读取文件然后把它们保存到一个数据库中(异步地)。你可以把这个过程想象成:先异步地从web服务器读取文件,读取完成后再把它们异步地保存到数据库中。
服务端应用的异步I/O
现在要展示的是两个普遍的情况,情况1(拉取)和情况2(推送)。
第一种情况同样是《Boost.Asio:客户端和服务端》中实现的异步服务端。在每一个异步操作的最后,我都会启动另外一个异步操作,这样 service.run() 就不会结束。下面展示的是裁剪过的框架代码,即 talk_to_client 类的主要方法:
void start() {
...
do_read(); // 首先,我们等待客户端登录
}
void on_read(const error_code & err, size_t bytes) {
std::string msg(read_buffer_, bytes);
if (msg.find("login ") == 0) on_login(msg);
else if ( msg.find("ping") == 0) on_ping();
else { ... }
}
void on_login(const std::string & msg) {
std::istringstream in(msg);
in >> username_ >> username_;
do_write("login ok\n");
}
void do_write(const std::string & msg) {
std::copy(msg.begin(), msg.end(), write_buffer_);
sock_.async_write_some( buffer(write_buffer_, msg.size()), MEM_FN2(on_write,_1,_2));
}
void on_write(const error_code & err, size_t bytes) { do_read(); }
简单来说,我们始终等待一个read操作,而且只要一发生,我们就处理然后将结果返回给客户端。
我们把上述代码进行修改就可以完成一个推送服务端:
void start() {
...
on_new_client_event();
}
void on_new_client_event() {
std::ostringstream msg;
msg << "client count " << clients.size();
for ( array::const_iterator b = clients.begin(), e = clients.end(); b != e; ++b)
(*b)->do_write(msg.str());
}
void on_read(const error_code & err, size_t bytes) {
std::string msg(read_buffer_, bytes);
// 在这里我们基本上只知道我们的客户端接收到我们的通知
}
void do_write(const std::string & msg) {
std::copy(msg.begin(), msg.end(), write_buffer_);
sock_.async_write_some( buffer(write_buffer_, msg.size()), MEM_FN2(on_write,_1,_2));
}
void on_write(const error_code & err, size_t bytes) { do_read(); }
只要有一个事件发生——比如 on_new_client_event——所有需要被通知的客户端就都会收到一条消息。当它们回复时,我们简单地认为它们已经确认收到事件。注意,等待中的异步操作永远不会耗尽(所以 service.run() 不会结束),因为我们始终还在等待一个新的客户端连接:
ip::tcp::acceptor acc(service, ip::tcp::endpoint(ip::tcp::v4(), 8001));
void handle_accept(talk_to_client::ptr client, const boost::system::error_code & err)
{
client->start();
talk_to_client::ptr new_client = talk_to_client::new_();
acc.async_accept(new_client->sock(), boost::bind(handle_accept, new_client, _1));
}
异步服务端中的多线程
《Boost.Asio:客户端和服务端》中展示的异步服务端是单线程的,所有的事情都发生在 main() 中:
int main() {
talk_to_client::ptr client = talk_to_client::new_();
acc.async_accept(client->sock(), boost::bind(handle_accept,client,_1));
service.run();
}
异步的美妙之处就在于可以非常简单地把单线程变为多线程。你可以一直保持单线程,直到并发客户端超过 200;然后,就可以用如下的代码片段把单线程改成 100 个线程:
boost::thread_group threads;
void listen_thread() {
service.run();
}
void start_listen(int thread_count) {
for (int i = 0; i < thread_count; ++i)
threads.create_thread(listen_thread);
}
int main(int argc, char* argv[]) {
talk_to_client::ptr client = talk_to_client::new_();
acc.async_accept(client->sock(), boost::bind(handle_accept, client, _1));
start_listen(100);
threads.join_all();
}
当然,一旦你选择了多线程,你就需要考虑线程安全。尽管你在某个线程里发起了 async_* 调用,但它的完成处理流程可能在另一个线程中被执行(因为那个线程也调用了 service.run())。就其本身而言这不是问题。只要你遵循逻辑流程——从 async_read() 到 on_read(),从 on_read() 到 process_request,从 process_request 到 async_write(),从 async_write() 到 on_write(),再从 on_write() 回到 async_read()——并且 talk_to_client 类中没有被并发调用的其他公有方法,那么尽管这些方法可能在不同线程中被调用,它们的调用顺序依然是有序的,因此不需要互斥量。
这也意味着对于一个客户端,只会有一个异步操作在等待。假如在某些情况,一个客户端有两个异步方法在等待,你就需要互斥量了。这是因为两个等待的操作可能正好在同一个时间完成,然后我们就会在两个不同的线程中间同时调用他们的完成处理函数。所以,这里需要线程安全,也就是需要使用互斥量。 在我们的异步服务端中,我们确实同时有两个等待的操作:
void do_read() {
async_read(sock_, buffer(read_buffer_),MEM_FN2(read_complete,_1,_2), MEM_FN2(on_read,_1,_2));
post_check_ping();
}
void post_check_ping() {
timer_.expires_from_now(boost::posix_time::millisec(5000));
timer_.async_wait( MEM_FN(on_check_ping));
}
当在读操作进行时,我们会同时异步等待两个完成:read 操作完成和超时检查。所以这里需要线程安全。
我的建议是:如果你准备使用多线程,从一开始就保证你的类是线程安全的,这通常不会影响性能(当然你也可以在配置中设置开关)。同时,如果你打算用多线程,那就从一开始就使用,这样能尽早发现可能存在的问题。一旦发现问题,你首先需要检查的就是:它在单线程运行时是否也会发生?如果是,问题比较简单,直接调试即可;否则,很可能是你忘了给某些方法加锁(互斥量)。
因为我们的例子需要是线程安全的,我已经把 talk_to_client 修改成了使用互斥量的版本。同时,我们还有一个客户端连接的列表,因为它有时会被访问,所以也需要自己的互斥量。
避免死锁和内存冲突不是那么容易。下面是我对 update_clients_changed() 方法所做的修改:
void update_clients_changed() {
array copy;
{
boost::recursive_mutex::scoped_lock lk(clients_cs); copy = clients;
}
for(array::iterator b = copy.begin(), e = copy.end(); b != e; ++b)
(*b)->set_clients_changed();
}
你需要避免的是同时有两个互斥量被锁定(这会导致死锁)。在我们的例子中,我们不想clients_cs和一个客户端的cs_互斥量同时被锁住。
异步操作
Boost.Asio 同样允许你异步地运行你的任何一个方法,只需要使用下面的代码片段:
void my_func() {
...
}
service.post(my_func);
这样就可以保证 my_func 在某个调用了 service.run() 的线程中被执行。你同样可以异步地调用一个带完成处理 handler 的方法,方法结束时 handler 会通知你。伪代码如下:
void on_complete() {
...
}
void my_func() {
...
service.post(on_complete);
}
async_call(my_func);
现成的库里并没有 async_call 这样的方法,因此你需要自己创建。幸运的是,它并不复杂,参考下面的代码片段:
struct async_op : boost::enable_shared_from_this<async_op>, ... {
typedef boost::function<void(boost::system::error_code)> completion_func;
typedef boost::function<boost::system::error_code ()> op_func;
struct operation { ... };
void start() {
{
boost::recursive_mutex::scoped_lock lk(cs_);
if (started_)
return;
started_ = true;
}
boost::thread t(boost::bind(&async_op::run,this));
}
void add(op_func op, completion_func completion, io_service &service) {
self_ = shared_from_this();
boost::recursive_mutex::scoped_lock lk(cs_);
ops_.push_back( operation(service, op, completion));
if (!started_) start();
}
void stop() {
boost::recursive_mutex::scoped_lock lk(cs_);
started_ = false; ops_.clear();
}
private:
boost::recursive_mutex cs_;
std::vector<operation> ops_;
bool started_;
ptr self_;
};
async_op 创建了一个后台线程,这个线程会依次运行(run())你添加(add())进去的所有异步操作。为了让事情简单一些,每个操作都由下面的 operation 结构体描述:
struct async_op : boost::enable_shared_from_this<async_op>, private boost::noncopyable {
struct operation {
operation(io_service & service, op_func op, completion_func completion) : service(&service), op(op), completion(completion) , work(new io_service::work(service)) {}
operation() : service(0) {}
io_service * service;
op_func op;
completion_func completion;
typedef boost::shared_ptr<io_service::work> work_ptr;
work_ptr work;
};
...
};
注意:只要还有操作在等待,我们就在 operation 的构造方法中构造一个 io_service::work 实例,从而保证在我们完成异步调用之前 service.run() 都不会结束(只要 io_service::work 实例保持活动,service.run() 就会认为还有工作要做)。后台线程的主体 run() 方法如下:
struct async_op : ... {
typedef boost::shared_ptr<async_op> ptr;
static ptr new_() { return ptr(new async_op); }
...
void run() {
while (true) {
{boost::recursive_mutex::scoped_lock lk(cs_);
if (!started_) break; }
boost::this_thread::sleep(boost::posix_time::millisec(10));
operation cur;
{boost::recursive_mutex::scoped_lock lk(cs_);
if (!ops_.empty()) {
cur = ops_[0];
ops_.erase(ops_.begin());
}
}
if (cur.service)
cur.service->post(boost::bind(cur.completion, cur.op()));
}
self_.reset();
}
};
run()方法就是后台线程;它仅仅观察是否有工作需要做,如果有,就一个一个地运行这些异步方法。在每个调用结束的时候,它会调用相关的完成处理方法。
为了测试,我们创建一个会被异步执行的 compute_file_checksum 方法(以 Windows 平台为例):
size_t checksum = 0;
boost::system::error_code compute_file_checksum(std::string file_name)
{
HANDLE file = ::CreateFile(file_name.c_str(),GENERIC_READ, 0, 0,OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, 0);
windows::random_access_handle h(service, file);
long buff[1024];
checksum = 0;
size_t bytes = 0, at = 0;
boost::system::error_code ec;
while ((bytes = read_at(h, at, buffer(buff), ec)) > 0) {
at += bytes; bytes /= sizeof(long);
for (size_t i = 0; i < bytes; ++i)
checksum += buff[i];
}
return boost::system::error_code(0,boost::system::generic_category());
}
void on_checksum(std::string file_name, boost::system::error_code) {
std::cout << "checksum for " << file_name << "=" << checksum << std::endl;
}
int main(int argc, char* argv[]) {
std::string fn = "readme.txt";
async_op::new_()->add(boost::bind(compute_file_checksum, fn), boost::bind(on_checksum, fn, _1), service);
service.run();
}
注意,我展示的只是实现异步调用一个方法的其中一种可能。除了像我这样实现一个后台线程,你也可以使用一个内部 io_service 实例,然后通过 post() 把异步方法投递给这个实例去调用。这个就作为练习留给读者。
你也可以扩展这个类让其可以展示一个异步操作的进度(比如,使用百分比)。这样做你就可以在主线程通过一个进度条来显示进度。
代理实现
代理一般位于客户端和服务端之间。它接受客户端的请求,可能会对请求进行修改,然后接着把请求发送到服务端。然后从服务端取回结果,可能也会对结果进行修改,然后接着把结果发送到客户端。
代理有什么特别的?我们讲述它的目的在于:对每个连接,你都需要两个 socket——一个给客户端,另外一个给服务端。这给实现一个代理增加了不小的难度。
实现一个同步的代理应用比异步的方式更复杂:数据可能同时从两端过来(客户端和服务端),也可能同时发往两端。这意味着如果我们选择同步实现,我们就很容易阻塞在某一端的 read() 或 write() 上,最终导致整个应用无响应。
下面就是一个实现异步代理的简单例子(我们沿用之前章节的 MEM_FN* 宏,这里额外需要一个三参数版本):
// #define MEM_FN3(x,y,z) boost::bind(&self_type::x, shared_from_this(), y, z)
class proxy : public boost::enable_shared_from_this<proxy> {
proxy(ip::tcp::endpoint ep_client, ip::tcp::endpoint ep_server) : ... {}
public:
typedef boost::shared_ptr<proxy> ptr;
static ptr start(ip::tcp::endpoint ep_client, ip::tcp::endpoint ep_svr) {
ptr new_(new proxy(ep_client, ep_svr));
// … 连接到两个端
return new_;
}
void stop() {
// ... 关闭两个连接
}
bool started() { return started_ == 2; }
private:
void on_connect(const boost::system::error_code & err) {
if (!err) {
if (++started_ == 2) on_start();
} else stop();
}
void on_start() {
do_read(client_, buff_client_);
do_read(server_, buff_server_);
}
...
private:
ip::tcp::socket client_, server_;
enum { max_msg = 1024 };
char buff_client_[max_msg], buff_server_[max_msg];
int started_;
};
这是个非常简单的代理。当我们两个端都连接时,它开始从两个端读取(on_start()方法):
class proxy : public boost::enable_shared_from_this<proxy> {
...
void on_read(ip::tcp::socket & sock, const boost::system::error_code& err, size_t bytes) {
char * buff = &sock == &client_ ? buff_client_ : buff_server_;
do_write(&sock == &client_ ? server_ : client_, buff, bytes);
}
void on_write(ip::tcp::socket & sock, const boost::system::error_code &err, size_t bytes){
if (&sock == &client_) do_read(server_, buff_server_);
else do_read(client_, buff_client_);
}
void do_read(ip::tcp::socket & sock, char* buff) {
async_read(sock, buffer(buff, max_msg), MEM_FN3(read_complete,ref(sock),_1,_2), MEM_FN3(on_read,ref(sock),_1,_2));
}
void do_write(ip::tcp::socket & sock, char * buff, size_t size) {
sock.async_write_some(buffer(buff,size), MEM_FN3(on_write,ref(sock),_1,_2));
}
size_t read_complete(ip::tcp::socket & sock, const boost::system::error_code & err, size_t bytes) {
if (sock.available() > 0) return sock.available();
return bytes > 0 ? 0 : 1;
}
};
对每一个成功的读取操作(on_read),它都会把消息发送到另一端。只要消息一发送成功(on_write),我们就从来源那一端再次读取。
使用下面的代码片段让这个流程运转起来:
int main(int argc, char* argv[]) {
ip::tcp::endpoint ep_c(ip::address::from_string("127.0.0.1"),8001);
ip::tcp::endpoint ep_s(ip::address::from_string("127.0.0.1"),8002);
proxy::start(ep_c, ep_s);
service.run();
}
你会注意到我在读和写之间重用了同一块 buffer。这样重用是没有问题的,因为从客户端读到的消息在新的消息被读取之前就已经写入到服务端了,反之亦然。不过这也意味着这种实现方式会遇到响应性问题:当我们正在处理向 B 端的写入时,我们不会从 A 端读取(要等到写入 B 端完成后才会重新从 A 端读取)。你可以通过重写实现来克服这个问题,我会把它当作练习留给你们。
小结
在选择同步或者异步时需要考虑很多事情。最先需要考虑的就是避免混淆它们。
在这一章中,我们已经看到了如何混合同步和异步编程、如何在客户端与服务端之间传递消息、客户端和服务端中的同步与异步I/O、异步操作以及代理的实现。
接下来,我们会深入 Boost.Asio 的多线程模型,请看本站的《Boost.Asio 多线程模型》。