设想一个常见的场景,我们设计一个服务器,每当到来一个新的请求时,我们需要读写一个全局的共享对象。在多线程环境下,多个线程同时操作一个共享的对象是不安全的。因此,我们需要对共享对象的操作进行同步。通用做法是用锁来保护,如果对其操作全部在asio的回调函数中进行,那么通常会用一个strand来使回调之间同步执行,方法是调用strand.wrap(handler)
或者asio::bind_executor(strand, handler)
。这也是strand的常见用法。
要记住的是strand只是保证回调之间的同步。另一种场景下我们需要异步操作 本身 不能被多个线程同时执行。以最常见的为asio::tcp::socket
为例,根据文档,socket对象的线程安全类型是:
Thread Safety
Distinct objects: Safe.
Shared objects: Unsafe.
即多个线程共享同一个socket对象时不保证线程安全。不过大部分情况我们不需要考虑线程安全的问题,因为我们通常会这样来使用(以下是简写):
do_read()
{
async_read(socket, buffer, on_read);
}
on_read()
{
process(buffer);
do_write();
}
do_write()
{
async_write(socket, buffer, on_write);
}
on_write()
{
do_read();
}
这种常规的回调链保证了socket
对象不被同时读写,没有线程安全问题。但是另外一些情况就不同了,假设我们加入异步超时机制(在这里吐槽一下,虽然官方文档中有timeout的例子,然而却是单线程的。asio的文档很多内容都太粗糙了),启动一个timer,当超过指定时间间隔时,在timer的回调中执行socket.cancel()
来取消该socket的事件循环。因为socket.cancel()
的时机是不确定的,另一个线程有可能同一时间执行到async_read/async_write之中,就很有可能引起线程安全问题。
这种情况,如果单纯的用strand来包装timer和socket的回调,是没用的,如前所说strand只保证其包装的回调之间的同步,而不能保证async_read/async_write本身的同步。因此这种情况,我们就需要把异步操作本身包装成回调,然后通过strand.dispatch/post来异步执行
do_read()
{
strand.post([=]()
{
async_read(socket, buffer, on_read);
});
}