理解asio中strand的用法

05 Jan 2019 | C/CPP, asio | | ˚C

设想一个常见的场景,我们设计一个服务器,每当到来一个新的请求时,我们需要读写一个全局的共享对象。在多线程环境下,多个线程同时操作一个共享的对象是不安全的。因此,我们需要对共享对象的操作进行同步。通用做法是用锁来保护,如果对其操作全部在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);
                });
}


Older · View Archive (15)

关于C函数能否抛出异常的讨论